yys 1 неделя назад
Родитель
Сommit
9ee82dde97
26 измененных файлов с 2176 добавлено и 2 удалено
  1. 11 0
      fs-admin-saas/src/main/resources/db/migration/tenant/V20260526_01__tenant_dict_columns.sql
  2. 243 0
      fs-admin/src/main/java/com/fs/admin/controller/TenantDictController.java
  3. 39 0
      fs-common/src/main/java/com/fs/common/core/domain/entity/SysDictData.java
  4. 39 0
      fs-common/src/main/java/com/fs/common/core/domain/entity/SysDictType.java
  5. 23 0
      fs-service/src/main/java/com/fs/system/constant/SysKeywordConstants.java
  6. 45 0
      fs-service/src/main/java/com/fs/tenant/dict/constant/TenantDictConstants.java
  7. 103 0
      fs-service/src/main/java/com/fs/tenant/dict/domain/TenantDictTemplateData.java
  8. 68 0
      fs-service/src/main/java/com/fs/tenant/dict/domain/TenantDictTemplateType.java
  9. 36 0
      fs-service/src/main/java/com/fs/tenant/dict/dto/TenantDictSyncRunReq.java
  10. 24 0
      fs-service/src/main/java/com/fs/tenant/dict/dto/TenantDictTemplateImportReq.java
  11. 95 0
      fs-service/src/main/java/com/fs/tenant/dict/helper/TenantDictContextHelper.java
  12. 42 0
      fs-service/src/main/java/com/fs/tenant/dict/mapper/TenantDictTemplateMapper.java
  13. 36 0
      fs-service/src/main/java/com/fs/tenant/dict/service/TenantDictManageService.java
  14. 20 0
      fs-service/src/main/java/com/fs/tenant/dict/service/TenantDictSyncService.java
  15. 43 0
      fs-service/src/main/java/com/fs/tenant/dict/service/TenantDictTemplateService.java
  16. 181 0
      fs-service/src/main/java/com/fs/tenant/dict/service/impl/TenantDictManageServiceImpl.java
  17. 552 0
      fs-service/src/main/java/com/fs/tenant/dict/service/impl/TenantDictSyncServiceImpl.java
  18. 325 0
      fs-service/src/main/java/com/fs/tenant/dict/service/impl/TenantDictTemplateServiceImpl.java
  19. 21 0
      fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictSyncResultVo.java
  20. 20 0
      fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictSyncTaskDetailVo.java
  21. 22 0
      fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictSyncTaskVo.java
  22. 18 0
      fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictTemplateImportResultVo.java
  23. 6 0
      fs-service/src/main/resources/db/tenant-initTable.sql
  24. 9 1
      fs-service/src/main/resources/mapper/system/SysDictDataMapper.xml
  25. 9 1
      fs-service/src/main/resources/mapper/system/SysDictTypeMapper.xml
  26. 146 0
      fs-service/src/main/resources/mapper/tenant/TenantDictTemplateMapper.xml

+ 11 - 0
fs-admin-saas/src/main/resources/db/migration/tenant/V20260526_01__tenant_dict_columns.sql

@@ -0,0 +1,11 @@
+-- 租户库 sys_dict 扩展字段(同步溯源)
+ALTER TABLE `sys_dict_type`
+    ADD COLUMN `dict_source` varchar(20) NULL DEFAULT 'tenant' COMMENT '来源 platform/tenant' AFTER `status`,
+    ADD COLUMN `is_platform_managed` tinyint NULL DEFAULT 0 COMMENT '是否平台管控 1是 0否' AFTER `dict_source`;
+
+ALTER TABLE `sys_dict_data`
+    ADD COLUMN `dict_source` varchar(20) NULL DEFAULT 'tenant' COMMENT '来源 platform/tenant' AFTER `status`,
+    ADD COLUMN `is_platform_managed` tinyint NULL DEFAULT 0 COMMENT '是否平台管控 1是 0否' AFTER `dict_source`;
+
+UPDATE `sys_dict_type` SET `dict_source` = 'tenant', `is_platform_managed` = 0 WHERE `dict_source` IS NULL;
+UPDATE `sys_dict_data` SET `dict_source` = 'tenant', `is_platform_managed` = 0 WHERE `dict_source` IS NULL;

+ 243 - 0
fs-admin/src/main/java/com/fs/admin/controller/TenantDictController.java

@@ -0,0 +1,243 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.domain.entity.SysDictType;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.tenant.dict.domain.TenantDictTemplateData;
+import com.fs.tenant.dict.domain.TenantDictTemplateType;
+import com.fs.tenant.dict.dto.TenantDictSyncRunReq;
+import com.fs.tenant.dict.dto.TenantDictTemplateImportReq;
+import com.fs.tenant.dict.service.TenantDictManageService;
+import com.fs.tenant.dict.service.TenantDictSyncService;
+import com.fs.tenant.dict.service.TenantDictTemplateService;
+import com.fs.tenant.dict.vo.TenantDictSyncTaskVo;
+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.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 总后台 - 租户字典管理(方案1+2)
+ */
+@RestController
+@RequestMapping("/tenant/dict")
+public class TenantDictController extends BaseController {
+
+    @Autowired
+    private TenantDictManageService tenantDictManageService;
+
+    @Autowired
+    private TenantDictTemplateService templateService;
+
+    @Autowired
+    private TenantDictSyncService syncService;
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:list')")
+    @GetMapping("/type/list")
+    public TableDataInfo tenantTypeList(@RequestParam Long tenantId, SysDictType query) {
+        startPage();
+        List<SysDictType> list = tenantDictManageService.selectDictTypeList(tenantId, query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:query')")
+    @GetMapping("/type/{dictId}")
+    public AjaxResult tenantTypeInfo(@PathVariable Long dictId, @RequestParam Long tenantId) {
+        return AjaxResult.success(tenantDictManageService.selectDictTypeById(tenantId, dictId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:add')")
+    @Log(title = "租户字典类型", businessType = BusinessType.INSERT)
+    @PostMapping("/type")
+    public AjaxResult tenantTypeAdd(@RequestParam Long tenantId, @Validated @RequestBody SysDictType dict) {
+        dict.setCreateBy(SecurityUtils.getUsername());
+        return toAjax(tenantDictManageService.insertDictType(tenantId, dict));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:edit')")
+    @Log(title = "租户字典类型", businessType = BusinessType.UPDATE)
+    @PutMapping("/type")
+    public AjaxResult tenantTypeEdit(@RequestParam Long tenantId, @Validated @RequestBody SysDictType dict) {
+        dict.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(tenantDictManageService.updateDictType(tenantId, dict));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:remove')")
+    @Log(title = "租户字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/type/{dictIds}")
+    public AjaxResult tenantTypeRemove(@PathVariable Long[] dictIds, @RequestParam Long tenantId) {
+        tenantDictManageService.deleteDictTypeByIds(tenantId, dictIds);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:list')")
+    @GetMapping("/data/list")
+    public TableDataInfo tenantDataList(@RequestParam Long tenantId, SysDictData query) {
+        startPage();
+        List<SysDictData> list = tenantDictManageService.selectDictDataList(tenantId, query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:query')")
+    @GetMapping("/data/{dictCode}")
+    public AjaxResult tenantDataInfo(@PathVariable Long dictCode, @RequestParam Long tenantId) {
+        return AjaxResult.success(tenantDictManageService.selectDictDataById(tenantId, dictCode));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:add')")
+    @Log(title = "租户字典数据", businessType = BusinessType.INSERT)
+    @PostMapping("/data")
+    public AjaxResult tenantDataAdd(@RequestParam Long tenantId, @Validated @RequestBody SysDictData dict) {
+        dict.setCreateBy(SecurityUtils.getUsername());
+        return toAjax(tenantDictManageService.insertDictData(tenantId, dict));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:edit')")
+    @Log(title = "租户字典数据", businessType = BusinessType.UPDATE)
+    @PutMapping("/data")
+    public AjaxResult tenantDataEdit(@RequestParam Long tenantId, @Validated @RequestBody SysDictData dict) {
+        dict.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(tenantDictManageService.updateDictData(tenantId, dict));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:remove')")
+    @Log(title = "租户字典数据", businessType = BusinessType.DELETE)
+    @DeleteMapping("/data/{dictCodes}")
+    public AjaxResult tenantDataRemove(@PathVariable Long[] dictCodes, @RequestParam Long tenantId) {
+        tenantDictManageService.deleteDictDataByIds(tenantId, dictCodes);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:edit')")
+    @DeleteMapping("/cache/refresh")
+    public AjaxResult refreshTenantCache(@RequestParam Long tenantId) {
+        tenantDictManageService.refreshDictCache(tenantId);
+        return AjaxResult.success("租户字典缓存已刷新");
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:list')")
+    @GetMapping("/template/type/list")
+    public TableDataInfo templateTypeList(TenantDictTemplateType query) {
+        startPage();
+        List<TenantDictTemplateType> list = templateService.selectTemplateTypeList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:query')")
+    @GetMapping("/template/type/{dictId}")
+    public AjaxResult templateTypeInfo(@PathVariable Long dictId) {
+        return AjaxResult.success(templateService.selectTemplateTypeById(dictId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:add')")
+    @Log(title = "平台字典模板类型", businessType = BusinessType.INSERT)
+    @PostMapping("/template/type")
+    public AjaxResult templateTypeAdd(@RequestBody TenantDictTemplateType row) {
+        row.setCreateBy(SecurityUtils.getUsername());
+        return toAjax(templateService.insertTemplateType(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:edit')")
+    @Log(title = "平台字典模板类型", businessType = BusinessType.UPDATE)
+    @PutMapping("/template/type")
+    public AjaxResult templateTypeEdit(@RequestBody TenantDictTemplateType row) {
+        row.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(templateService.updateTemplateType(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:remove')")
+    @Log(title = "平台字典模板类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/template/type/{dictIds}")
+    public AjaxResult templateTypeRemove(@PathVariable Long[] dictIds) {
+        templateService.deleteTemplateTypeByIds(dictIds);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:list')")
+    @GetMapping("/template/data/list")
+    public TableDataInfo templateDataList(TenantDictTemplateData query) {
+        startPage();
+        List<TenantDictTemplateData> list = templateService.selectTemplateDataList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:query')")
+    @GetMapping("/template/data/{dictCode}")
+    public AjaxResult templateDataInfo(@PathVariable Long dictCode) {
+        return AjaxResult.success(templateService.selectTemplateDataById(dictCode));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:add')")
+    @Log(title = "平台字典模板数据", businessType = BusinessType.INSERT)
+    @PostMapping("/template/data")
+    public AjaxResult templateDataAdd(@RequestBody TenantDictTemplateData row) {
+        row.setCreateBy(SecurityUtils.getUsername());
+        return toAjax(templateService.insertTemplateData(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:edit')")
+    @Log(title = "平台字典模板数据", businessType = BusinessType.UPDATE)
+    @PutMapping("/template/data")
+    public AjaxResult templateDataEdit(@RequestBody TenantDictTemplateData row) {
+        row.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(templateService.updateTemplateData(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:remove')")
+    @Log(title = "平台字典模板数据", businessType = BusinessType.DELETE)
+    @DeleteMapping("/template/data/{dictCodes}")
+    public AjaxResult templateDataRemove(@PathVariable Long[] dictCodes) {
+        templateService.deleteTemplateDataByIds(dictCodes);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:list')")
+    @GetMapping("/template/type/optionselect")
+    public AjaxResult templateTypeOptions() {
+        return AjaxResult.success(templateService.selectAllTemplateTypes());
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:add')")
+    @Log(title = "平台字典模板导入", businessType = BusinessType.INSERT)
+    @PostMapping("/template/import/platform")
+    public AjaxResult importFromPlatform(@RequestBody TenantDictTemplateImportReq req) {
+        return AjaxResult.success(templateService.importFromPlatform(req, SecurityUtils.getUsername()));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:template:add')")
+    @Log(title = "平台字典模板导入", businessType = BusinessType.INSERT)
+    @PostMapping("/template/import/tenant")
+    public AjaxResult importFromTenant(@RequestBody TenantDictTemplateImportReq req) {
+        return AjaxResult.success(templateService.importFromTenant(req, SecurityUtils.getUsername()));
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:sync')")
+    @Log(title = "租户字典同步", businessType = BusinessType.OTHER)
+    @PostMapping("/sync/run")
+    public AjaxResult syncRun(@RequestBody TenantDictSyncRunReq req) {
+        Map<String, Object> data = syncService.runSync(req, SecurityUtils.getUsername());
+        return AjaxResult.success(data);
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:sync')")
+    @GetMapping("/sync/task/{taskNo}")
+    public AjaxResult syncTask(@PathVariable String taskNo) {
+        TenantDictSyncTaskVo vo = syncService.getTask(taskNo);
+        return AjaxResult.success(vo);
+    }
+
+    @PreAuthorize("@ss.hasPermi('tenant:dict:sync')")
+    @GetMapping("/sync/preview")
+    public AjaxResult syncPreview(@RequestParam Long tenantId,
+                                  @RequestParam(required = false) List<String> dictTypes) {
+        return AjaxResult.success(syncService.previewDiff(tenantId, dictTypes));
+    }
+}

+ 39 - 0
fs-common/src/main/java/com/fs/common/core/domain/entity/SysDictData.java

@@ -52,6 +52,15 @@ public class SysDictData extends BaseEntity
     @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
     private String status;
 
+    /** 来源 platform/tenant */
+    private String dictSource;
+
+    /** 是否平台管控 1是 0否 */
+    private Integer isPlatformManaged;
+
+    /** 租户ID(总后台查询用,非表字段) */
+    private Long tenantId;
+
     public Long getDictCode()
     {
         return dictCode;
@@ -153,6 +162,36 @@ public class SysDictData extends BaseEntity
     {
         this.status = status;
     }
+
+    public String getDictSource()
+    {
+        return dictSource;
+    }
+
+    public void setDictSource(String dictSource)
+    {
+        this.dictSource = dictSource;
+    }
+
+    public Integer getIsPlatformManaged()
+    {
+        return isPlatformManaged;
+    }
+
+    public void setIsPlatformManaged(Integer isPlatformManaged)
+    {
+        this.isPlatformManaged = isPlatformManaged;
+    }
+
+    public Long getTenantId()
+    {
+        return tenantId;
+    }
+
+    public void setTenantId(Long tenantId)
+    {
+        this.tenantId = tenantId;
+    }
     
     @Override
     public String toString() {

+ 39 - 0
fs-common/src/main/java/com/fs/common/core/domain/entity/SysDictType.java

@@ -33,6 +33,15 @@ public class SysDictType extends BaseEntity
     @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
     private String status;
 
+    /** 来源 platform/tenant */
+    private String dictSource;
+
+    /** 是否平台管控 1是 0否 */
+    private Integer isPlatformManaged;
+
+    /** 租户ID(总后台查询用,非表字段) */
+    private Long tenantId;
+
     public Long getDictId()
     {
         return dictId;
@@ -76,6 +85,36 @@ public class SysDictType extends BaseEntity
     {
         this.status = status;
     }
+
+    public String getDictSource()
+    {
+        return dictSource;
+    }
+
+    public void setDictSource(String dictSource)
+    {
+        this.dictSource = dictSource;
+    }
+
+    public Integer getIsPlatformManaged()
+    {
+        return isPlatformManaged;
+    }
+
+    public void setIsPlatformManaged(Integer isPlatformManaged)
+    {
+        this.isPlatformManaged = isPlatformManaged;
+    }
+
+    public Long getTenantId()
+    {
+        return tenantId;
+    }
+
+    public void setTenantId(Long tenantId)
+    {
+        this.tenantId = tenantId;
+    }
     
     @Override
     public String toString() {

+ 23 - 0
fs-service/src/main/java/com/fs/system/constant/SysKeywordConstants.java

@@ -0,0 +1,23 @@
+package com.fs.system.constant;
+
+/**
+ * 系统关键字常量
+ */
+public final class SysKeywordConstants {
+
+    private SysKeywordConstants() {
+    }
+
+    /** 字典类型 */
+    public static final String DICT_TYPE = "keyword_type";
+
+    /** 看课弹幕 */
+    public static final int TYPE_COURSE_COMMENT = 1;
+
+    public static final String REDIS_KEY_PREFIX = "sys:keywords:";
+
+    public static String redisKey(Integer keywordType) {
+        int type = keywordType != null ? keywordType : TYPE_COURSE_COMMENT;
+        return REDIS_KEY_PREFIX + type;
+    }
+}

+ 45 - 0
fs-service/src/main/java/com/fs/tenant/dict/constant/TenantDictConstants.java

@@ -0,0 +1,45 @@
+package com.fs.tenant.dict.constant;
+
+/**
+ * 租户字典模块常量
+ */
+public final class TenantDictConstants {
+
+    private TenantDictConstants() {
+    }
+
+    /** 字典来源:平台模板同步 */
+    public static final String SOURCE_PLATFORM = "platform";
+
+    /** 字典来源:租户/总后台手工维护 */
+    public static final String SOURCE_TENANT = "tenant";
+
+    /** 同步模式:仅追加缺失项 */
+    public static final String SYNC_APPEND = "APPEND";
+
+    /** 同步模式:合并更新平台管控项,保留租户扩展项 */
+    public static final String SYNC_MERGE = "MERGE";
+
+    /** 同步模式:按字典类型覆盖平台管控项(保留租户自有项) */
+    public static final String SYNC_OVERWRITE = "OVERWRITE";
+
+    /** 租户范围:全部启用租户 */
+    public static final String SCOPE_ALL = "ALL";
+
+    /** 租户范围:仅包含指定租户 */
+    public static final String SCOPE_INCLUDE = "INCLUDE";
+
+    /** 租户范围:排除指定租户 */
+    public static final String SCOPE_EXCLUDE = "EXCLUDE";
+
+    /** 任务状态 */
+    public static final String TASK_RUNNING = "RUNNING";
+    public static final String TASK_SUCCESS = "SUCCESS";
+    public static final String TASK_PARTIAL = "PARTIAL";
+    public static final String TASK_FAILED = "FAILED";
+
+    /** 明细状态 */
+    public static final String DETAIL_SUCCESS = "SUCCESS";
+    public static final String DETAIL_FAILED = "FAILED";
+    public static final String DETAIL_SKIPPED = "SKIPPED";
+}

+ 103 - 0
fs-service/src/main/java/com/fs/tenant/dict/domain/TenantDictTemplateData.java

@@ -0,0 +1,103 @@
+package com.fs.tenant.dict.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+
+/**
+ * 平台字典数据模板 tenant_dict_template_data(主库)
+ */
+public class TenantDictTemplateData extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long dictCode;
+    private Long dictSort;
+    private String dictLabel;
+    private String dictValue;
+    private String dictType;
+    private String cssClass;
+    private String listClass;
+    private String isDefault;
+    private String status;
+    /** 是否平台强制管控 1是 0否 */
+    private Integer isManaged;
+
+    public Long getDictCode() {
+        return dictCode;
+    }
+
+    public void setDictCode(Long dictCode) {
+        this.dictCode = dictCode;
+    }
+
+    public Long getDictSort() {
+        return dictSort;
+    }
+
+    public void setDictSort(Long dictSort) {
+        this.dictSort = dictSort;
+    }
+
+    public String getDictLabel() {
+        return dictLabel;
+    }
+
+    public void setDictLabel(String dictLabel) {
+        this.dictLabel = dictLabel;
+    }
+
+    public String getDictValue() {
+        return dictValue;
+    }
+
+    public void setDictValue(String dictValue) {
+        this.dictValue = dictValue;
+    }
+
+    public String getDictType() {
+        return dictType;
+    }
+
+    public void setDictType(String dictType) {
+        this.dictType = dictType;
+    }
+
+    public String getCssClass() {
+        return cssClass;
+    }
+
+    public void setCssClass(String cssClass) {
+        this.cssClass = cssClass;
+    }
+
+    public String getListClass() {
+        return listClass;
+    }
+
+    public void setListClass(String listClass) {
+        this.listClass = listClass;
+    }
+
+    public String getIsDefault() {
+        return isDefault;
+    }
+
+    public void setIsDefault(String isDefault) {
+        this.isDefault = isDefault;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public Integer getIsManaged() {
+        return isManaged;
+    }
+
+    public void setIsManaged(Integer isManaged) {
+        this.isManaged = isManaged;
+    }
+}

+ 68 - 0
fs-service/src/main/java/com/fs/tenant/dict/domain/TenantDictTemplateType.java

@@ -0,0 +1,68 @@
+package com.fs.tenant.dict.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+
+/**
+ * 平台字典类型模板 tenant_dict_template_type(主库)
+ */
+public class TenantDictTemplateType extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long dictId;
+    private String dictName;
+    private String dictType;
+    private String status;
+    /** 是否平台强制管控 1是 0否 */
+    private Integer isManaged;
+    /** 默认同步模式 APPEND/MERGE/OVERWRITE */
+    private String syncMode;
+
+    public Long getDictId() {
+        return dictId;
+    }
+
+    public void setDictId(Long dictId) {
+        this.dictId = dictId;
+    }
+
+    public String getDictName() {
+        return dictName;
+    }
+
+    public void setDictName(String dictName) {
+        this.dictName = dictName;
+    }
+
+    public String getDictType() {
+        return dictType;
+    }
+
+    public void setDictType(String dictType) {
+        this.dictType = dictType;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public Integer getIsManaged() {
+        return isManaged;
+    }
+
+    public void setIsManaged(Integer isManaged) {
+        this.isManaged = isManaged;
+    }
+
+    public String getSyncMode() {
+        return syncMode;
+    }
+
+    public void setSyncMode(String syncMode) {
+        this.syncMode = syncMode;
+    }
+}

+ 36 - 0
fs-service/src/main/java/com/fs/tenant/dict/dto/TenantDictSyncRunReq.java

@@ -0,0 +1,36 @@
+package com.fs.tenant.dict.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 租户字典同步请求
+ */
+@Data
+public class TenantDictSyncRunReq {
+
+    /** 同步模式 APPEND / MERGE / OVERWRITE */
+    private String syncMode;
+
+    /** 租户范围 ALL / INCLUDE / EXCLUDE */
+    private String scopeType;
+
+    /** 指定租户ID列表(INCLUDE/EXCLUDE 时使用) */
+    private List<Long> tenantIds;
+
+    /** 指定字典类型,空=全部模板类型 */
+    private List<String> dictTypes;
+
+    /** 是否并行执行 */
+    private Boolean parallel;
+
+    /** 并行线程数 */
+    private Integer threads;
+
+    /** 仅预览差异,不实际写入 */
+    private Boolean dryRun;
+
+    /** OVERWRITE 模式二次确认 */
+    private Boolean overwriteConfirm;
+}

+ 24 - 0
fs-service/src/main/java/com/fs/tenant/dict/dto/TenantDictTemplateImportReq.java

@@ -0,0 +1,24 @@
+package com.fs.tenant.dict.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 从平台主库/租户库导入字典到平台模板
+ */
+@Data
+public class TenantDictTemplateImportReq {
+
+    /** 租户库导入时必填 */
+    private Long tenantId;
+
+    /** 按字典类型导入(推荐) */
+    private List<String> dictTypes;
+
+    /** 平台主库按 dictId 导入(与 dictTypes 二选一) */
+    private List<Long> dictIds;
+
+    /** 模板已存在同类型时:true=覆盖类型+合并数据,false=跳过类型仅追加缺失数据项 */
+    private Boolean overwriteExisting;
+}

+ 95 - 0
fs-service/src/main/java/com/fs/tenant/dict/helper/TenantDictContextHelper.java

@@ -0,0 +1,95 @@
+package com.fs.tenant.dict.helper;
+
+import com.fs.common.config.RedisTenantContext;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.exception.CustomException;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.function.Supplier;
+
+/**
+ * 租户字典上下文工具:统一切库、Redis 租户前缀、资源释放。
+ * <p>
+ * 所有租户字典读写必须通过本类,避免数据源泄漏和 Redis 缓存串租户。
+ */
+@Component
+public class TenantDictContextHelper {
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+
+    /**
+     * 在租户库上下文中执行(无返回值)
+     */
+    public void runInTenant(Long tenantId, Runnable action) {
+        executeInTenant(tenantId, () -> {
+            action.run();
+            return null;
+        });
+    }
+
+    /**
+     * 在租户库上下文中执行(有返回值)
+     */
+    public <T> T executeInTenant(Long tenantId, Supplier<T> action) {
+        TenantInfo tenant = loadActiveTenant(tenantId);
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            tenantDataSourceManager.switchTenant(tenant);
+            RedisTenantContext.setTenantId(tenantId);
+            return action.get();
+        } finally {
+            RedisTenantContext.clear();
+            tenantDataSourceManager.clear();
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+
+    /**
+     * 在主库上下文中执行
+     */
+    public <T> T executeInMaster(Supplier<T> action) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            return action.get();
+        } finally {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+
+    public void runInMaster(Runnable action) {
+        executeInMaster(() -> {
+            action.run();
+            return null;
+        });
+    }
+
+    /**
+     * 校验并加载启用中的租户
+     */
+    public TenantInfo loadActiveTenant(Long tenantId) {
+        if (tenantId == null) {
+            throw new CustomException("租户ID不能为空");
+        }
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        TenantInfo tenant = tenantInfoMapper.selectTenantInfoById(String.valueOf(tenantId));
+        if (tenant == null) {
+            throw new CustomException("租户不存在: " + tenantId);
+        }
+        if (tenant.getStatus() != null && tenant.getStatus() == 2) {
+            throw new CustomException("租户初始化中,暂不可操作字典");
+        }
+        if (tenant.getStatus() == null || tenant.getStatus() != 1) {
+            throw new CustomException("租户未启用,暂不可操作字典");
+        }
+        return tenant;
+    }
+}

+ 42 - 0
fs-service/src/main/java/com/fs/tenant/dict/mapper/TenantDictTemplateMapper.java

@@ -0,0 +1,42 @@
+package com.fs.tenant.dict.mapper;
+
+import com.fs.tenant.dict.domain.TenantDictTemplateData;
+import com.fs.tenant.dict.domain.TenantDictTemplateType;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 平台字典模板 Mapper(主库 tenant_dict_template_*)
+ */
+public interface TenantDictTemplateMapper {
+
+    List<TenantDictTemplateType> selectTemplateTypeList(TenantDictTemplateType query);
+
+    TenantDictTemplateType selectTemplateTypeById(Long dictId);
+
+    TenantDictTemplateType selectTemplateTypeByType(String dictType);
+
+    int insertTemplateType(TenantDictTemplateType row);
+
+    int updateTemplateType(TenantDictTemplateType row);
+
+    int deleteTemplateTypeByIds(Long[] dictIds);
+
+    List<TenantDictTemplateData> selectTemplateDataList(TenantDictTemplateData query);
+
+    TenantDictTemplateData selectTemplateDataById(Long dictCode);
+
+    TenantDictTemplateData selectTemplateDataByTypeAndValue(@Param("dictType") String dictType,
+                                                            @Param("dictValue") String dictValue);
+
+    List<TenantDictTemplateData> selectTemplateDataByType(String dictType);
+
+    int insertTemplateData(TenantDictTemplateData row);
+
+    int updateTemplateData(TenantDictTemplateData row);
+
+    int deleteTemplateDataByIds(Long[] dictCodes);
+
+    int countTemplateDataByType(String dictType);
+}

+ 36 - 0
fs-service/src/main/java/com/fs/tenant/dict/service/TenantDictManageService.java

@@ -0,0 +1,36 @@
+package com.fs.tenant.dict.service;
+
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.domain.entity.SysDictType;
+
+import java.util.List;
+
+/**
+ * 租户字典管理服务(方案1:总后台代管单个租户字典)
+ */
+public interface TenantDictManageService {
+
+    List<SysDictType> selectDictTypeList(Long tenantId, SysDictType query);
+
+    SysDictType selectDictTypeById(Long tenantId, Long dictId);
+
+    int insertDictType(Long tenantId, SysDictType dict);
+
+    int updateDictType(Long tenantId, SysDictType dict);
+
+    void deleteDictTypeByIds(Long tenantId, Long[] dictIds);
+
+    List<SysDictData> selectDictDataList(Long tenantId, SysDictData query);
+
+    SysDictData selectDictDataById(Long tenantId, Long dictCode);
+
+    int insertDictData(Long tenantId, SysDictData dict);
+
+    int updateDictData(Long tenantId, SysDictData dict);
+
+    void deleteDictDataByIds(Long tenantId, Long[] dictCodes);
+
+    void refreshDictCache(Long tenantId);
+
+    String checkDictTypeUnique(Long tenantId, SysDictType dict);
+}

+ 20 - 0
fs-service/src/main/java/com/fs/tenant/dict/service/TenantDictSyncService.java

@@ -0,0 +1,20 @@
+package com.fs.tenant.dict.service;
+
+import com.fs.tenant.dict.dto.TenantDictSyncRunReq;
+import com.fs.tenant.dict.vo.TenantDictSyncResultVo;
+import com.fs.tenant.dict.vo.TenantDictSyncTaskVo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 租户字典同步服务(方案2:模板下发到租户库)
+ */
+public interface TenantDictSyncService {
+
+    Map<String, Object> runSync(TenantDictSyncRunReq req, String triggerBy);
+
+    TenantDictSyncTaskVo getTask(String taskNo);
+
+    List<TenantDictSyncResultVo> previewDiff(Long tenantId, List<String> dictTypes);
+}

+ 43 - 0
fs-service/src/main/java/com/fs/tenant/dict/service/TenantDictTemplateService.java

@@ -0,0 +1,43 @@
+package com.fs.tenant.dict.service;
+
+import com.fs.tenant.dict.domain.TenantDictTemplateData;
+import com.fs.tenant.dict.domain.TenantDictTemplateType;
+
+import com.fs.tenant.dict.dto.TenantDictTemplateImportReq;
+import com.fs.tenant.dict.vo.TenantDictTemplateImportResultVo;
+
+import java.util.List;
+
+/**
+ * 平台字典模板服务(主库)
+ */
+public interface TenantDictTemplateService {
+
+    List<TenantDictTemplateType> selectTemplateTypeList(TenantDictTemplateType query);
+
+    TenantDictTemplateType selectTemplateTypeById(Long dictId);
+
+    int insertTemplateType(TenantDictTemplateType row);
+
+    int updateTemplateType(TenantDictTemplateType row);
+
+    void deleteTemplateTypeByIds(Long[] dictIds);
+
+    List<TenantDictTemplateData> selectTemplateDataList(TenantDictTemplateData query);
+
+    TenantDictTemplateData selectTemplateDataById(Long dictCode);
+
+    int insertTemplateData(TenantDictTemplateData row);
+
+    int updateTemplateData(TenantDictTemplateData row);
+
+    void deleteTemplateDataByIds(Long[] dictCodes);
+
+    List<TenantDictTemplateType> selectAllTemplateTypes();
+
+    List<TenantDictTemplateData> selectTemplateDataByType(String dictType);
+
+    TenantDictTemplateImportResultVo importFromPlatform(TenantDictTemplateImportReq req, String operator);
+
+    TenantDictTemplateImportResultVo importFromTenant(TenantDictTemplateImportReq req, String operator);
+}

+ 181 - 0
fs-service/src/main/java/com/fs/tenant/dict/service/impl/TenantDictManageServiceImpl.java

@@ -0,0 +1,181 @@
+package com.fs.tenant.dict.service.impl;
+
+import com.fs.common.constant.UserConstants;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.domain.entity.SysDictType;
+import com.fs.common.exception.CustomException;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.system.service.ISysDictTypeService;
+import com.fs.tenant.dict.constant.TenantDictConstants;
+import com.fs.tenant.dict.helper.TenantDictContextHelper;
+import com.fs.tenant.dict.service.TenantDictManageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 租户字典代管:在指定租户库内复用标准字典 Service。
+ */
+@Service
+public class TenantDictManageServiceImpl implements TenantDictManageService {
+
+    @Autowired
+    private TenantDictContextHelper contextHelper;
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @Autowired
+    private ISysDictDataService dictDataService;
+
+    @Override
+    public List<SysDictType> selectDictTypeList(Long tenantId, SysDictType query) {
+        return contextHelper.executeInTenant(tenantId, () -> dictTypeService.selectDictTypeList(query));
+    }
+
+    @Override
+    public SysDictType selectDictTypeById(Long tenantId, Long dictId) {
+        return contextHelper.executeInTenant(tenantId, () -> dictTypeService.selectDictTypeById(dictId));
+    }
+
+    @Override
+    public int insertDictType(Long tenantId, SysDictType dict) {
+        markTenantManual(dict);
+        return contextHelper.executeInTenant(tenantId, () -> {
+            if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) {
+                throw new CustomException("字典类型已存在: " + dict.getDictType());
+            }
+            return dictTypeService.insertDictType(dict);
+        });
+    }
+
+    @Override
+    public int updateDictType(Long tenantId, SysDictType dict) {
+        return contextHelper.executeInTenant(tenantId, () -> {
+            SysDictType existing = dictTypeService.selectDictTypeById(dict.getDictId());
+            if (existing == null) {
+                throw new CustomException("字典类型不存在");
+            }
+            if (isPlatformLocked(existing) && !TenantDictConstants.SOURCE_PLATFORM.equals(dict.getDictSource())) {
+                throw new CustomException("平台管控字典类型不可直接修改,请通过模板同步或先取消平台管控");
+            }
+            if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) {
+                throw new CustomException("字典类型已存在: " + dict.getDictType());
+            }
+            if (dict.getDictSource() == null) {
+                dict.setDictSource(existing.getDictSource());
+            }
+            if (dict.getIsPlatformManaged() == null) {
+                dict.setIsPlatformManaged(existing.getIsPlatformManaged());
+            }
+            return dictTypeService.updateDictType(dict);
+        });
+    }
+
+    @Override
+    public void deleteDictTypeByIds(Long tenantId, Long[] dictIds) {
+        contextHelper.runInTenant(tenantId, () -> {
+            for (Long dictId : dictIds) {
+                SysDictType existing = dictTypeService.selectDictTypeById(dictId);
+                if (existing != null && isPlatformLocked(existing)) {
+                    throw new CustomException("平台管控字典类型不可删除: " + existing.getDictType());
+                }
+            }
+            dictTypeService.deleteDictTypeByIds(dictIds);
+        });
+    }
+
+    @Override
+    public List<SysDictData> selectDictDataList(Long tenantId, SysDictData query) {
+        return contextHelper.executeInTenant(tenantId, () -> dictDataService.selectDictDataList(query));
+    }
+
+    @Override
+    public SysDictData selectDictDataById(Long tenantId, Long dictCode) {
+        return contextHelper.executeInTenant(tenantId, () -> dictDataService.selectDictDataById(dictCode));
+    }
+
+    @Override
+    public int insertDictData(Long tenantId, SysDictData dict) {
+        markTenantManual(dict);
+        return contextHelper.executeInTenant(tenantId, () -> dictDataService.insertDictData(dict));
+    }
+
+    @Override
+    public int updateDictData(Long tenantId, SysDictData dict) {
+        return contextHelper.executeInTenant(tenantId, () -> {
+            SysDictData existing = dictDataService.selectDictDataById(dict.getDictCode());
+            if (existing == null) {
+                throw new CustomException("字典数据不存在");
+            }
+            if (isPlatformLocked(existing)) {
+                throw new CustomException("平台管控字典数据不可直接修改: " + existing.getDictLabel());
+            }
+            if (dict.getDictSource() == null) {
+                dict.setDictSource(existing.getDictSource());
+            }
+            if (dict.getIsPlatformManaged() == null) {
+                dict.setIsPlatformManaged(existing.getIsPlatformManaged());
+            }
+            return dictDataService.updateDictData(dict);
+        });
+    }
+
+    @Override
+    public void deleteDictDataByIds(Long tenantId, Long[] dictCodes) {
+        contextHelper.runInTenant(tenantId, () -> {
+            for (Long dictCode : dictCodes) {
+                SysDictData existing = dictDataService.selectDictDataById(dictCode);
+                if (existing != null && isPlatformLocked(existing)) {
+                    throw new CustomException("平台管控字典数据不可删除: " + existing.getDictLabel());
+                }
+            }
+            dictDataService.deleteDictDataByIds(dictCodes);
+        });
+    }
+
+    @Override
+    public void refreshDictCache(Long tenantId) {
+        contextHelper.runInTenant(tenantId, () -> dictTypeService.resetDictCache());
+    }
+
+    @Override
+    public String checkDictTypeUnique(Long tenantId, SysDictType dict) {
+        return contextHelper.executeInTenant(tenantId, () -> dictTypeService.checkDictTypeUnique(dict));
+    }
+
+    private void markTenantManual(SysDictType dict) {
+        if (dict.getDictSource() == null) {
+            dict.setDictSource(TenantDictConstants.SOURCE_TENANT);
+        }
+        if (dict.getIsPlatformManaged() == null) {
+            dict.setIsPlatformManaged(0);
+        }
+        if (dict.getStatus() == null) {
+            dict.setStatus(UserConstants.NORMAL);
+        }
+    }
+
+    private void markTenantManual(SysDictData dict) {
+        if (dict.getDictSource() == null) {
+            dict.setDictSource(TenantDictConstants.SOURCE_TENANT);
+        }
+        if (dict.getIsPlatformManaged() == null) {
+            dict.setIsPlatformManaged(0);
+        }
+        if (dict.getStatus() == null) {
+            dict.setStatus(UserConstants.NORMAL);
+        }
+    }
+
+    private boolean isPlatformLocked(SysDictType type) {
+        return TenantDictConstants.SOURCE_PLATFORM.equals(type.getDictSource())
+                || (type.getIsPlatformManaged() != null && type.getIsPlatformManaged() == 1);
+    }
+
+    private boolean isPlatformLocked(SysDictData data) {
+        return TenantDictConstants.SOURCE_PLATFORM.equals(data.getDictSource())
+                || (data.getIsPlatformManaged() != null && data.getIsPlatformManaged() == 1);
+    }
+}

+ 552 - 0
fs-service/src/main/java/com/fs/tenant/dict/service/impl/TenantDictSyncServiceImpl.java

@@ -0,0 +1,552 @@
+package com.fs.tenant.dict.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.domain.entity.SysDictType;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.system.mapper.SysDictDataMapper;
+import com.fs.system.mapper.SysDictTypeMapper;
+import com.fs.system.service.ISysDictTypeService;
+import com.fs.tenant.dict.constant.TenantDictConstants;
+import com.fs.tenant.dict.domain.TenantDictTemplateData;
+import com.fs.tenant.dict.domain.TenantDictTemplateType;
+import com.fs.tenant.dict.dto.TenantDictSyncRunReq;
+import com.fs.tenant.dict.helper.TenantDictContextHelper;
+import com.fs.tenant.dict.service.TenantDictSyncService;
+import com.fs.tenant.dict.service.TenantDictTemplateService;
+import com.fs.tenant.dict.vo.TenantDictSyncResultVo;
+import com.fs.tenant.dict.vo.TenantDictSyncTaskDetailVo;
+import com.fs.tenant.dict.vo.TenantDictSyncTaskVo;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.service.TenantInfoService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+/**
+ * 租户字典同步引擎:模板 → 租户库,支持 APPEND / MERGE / OVERWRITE。
+ */
+@Service
+public class TenantDictSyncServiceImpl implements TenantDictSyncService {
+
+    private static final Logger log = LoggerFactory.getLogger(TenantDictSyncServiceImpl.class);
+
+    @Autowired
+    private TenantDictTemplateService templateService;
+    @Autowired
+    private TenantDictContextHelper contextHelper;
+    @Autowired
+    private TenantInfoService tenantInfoService;
+    @Autowired
+    private SysDictTypeMapper dictTypeMapper;
+    @Autowired
+    private SysDictDataMapper dictDataMapper;
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @Override
+    public Map<String, Object> runSync(TenantDictSyncRunReq req, String triggerBy) {
+        validateSyncReq(req);
+        List<TenantDictTemplateType> templateTypes = resolveTemplateTypes(req.getDictTypes());
+        if (templateTypes.isEmpty()) {
+            throw new CustomException("没有可同步的模板字典类型,请先在平台模板中维护");
+        }
+        List<TenantInfo> tenants = resolveTenants(req);
+        if (tenants.isEmpty()) {
+            throw new CustomException("未匹配到可同步租户");
+        }
+
+        if (Boolean.TRUE.equals(req.getDryRun())) {
+            List<TenantDictSyncResultVo> preview = new ArrayList<>();
+            for (TenantInfo tenant : tenants) {
+                preview.add(previewOneTenant(tenant, templateTypes, req.getSyncMode()));
+            }
+            Map<String, Object> resp = new HashMap<>();
+            resp.put("dryRun", true);
+            resp.put("preview", preview);
+            return resp;
+        }
+
+        String taskNo = buildTaskNo();
+        contextHelper.runInMaster(() -> {
+            initMasterTaskTables();
+            insertTask(taskNo, req, tenants.size(), triggerBy);
+        });
+
+        CompletableFuture.runAsync(() -> executeSyncTask(taskNo, req, tenants, templateTypes));
+
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("taskNo", taskNo);
+        resp.put("status", TenantDictConstants.TASK_RUNNING);
+        resp.put("totalTenants", tenants.size());
+        resp.put("syncMode", req.getSyncMode());
+        return resp;
+    }
+
+    @Override
+    public TenantDictSyncTaskVo getTask(String taskNo) {
+        return contextHelper.executeInMaster(() -> {
+            initMasterTaskTables();
+            List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+                    "SELECT task_no,sync_mode,scope_type,dict_types,total_tenants,success_tenants,failed_tenants,status,trigger_by,started_at,finished_at " +
+                            "FROM tenant_dict_sync_task WHERE task_no=?", taskNo);
+            if (rows.isEmpty()) {
+                return null;
+            }
+            Map<String, Object> row = rows.get(0);
+            TenantDictSyncTaskVo vo = new TenantDictSyncTaskVo();
+            vo.setTaskNo(str(row.get("task_no")));
+            vo.setSyncMode(str(row.get("sync_mode")));
+            vo.setScopeType(str(row.get("scope_type")));
+            vo.setDictTypes(str(row.get("dict_types")));
+            vo.setTotalTenants(intVal(row.get("total_tenants")));
+            vo.setSuccessTenants(intVal(row.get("success_tenants")));
+            vo.setFailedTenants(intVal(row.get("failed_tenants")));
+            vo.setStatus(str(row.get("status")));
+            vo.setTriggerBy(str(row.get("trigger_by")));
+            vo.setStartedAt(str(row.get("started_at")));
+            vo.setFinishedAt(str(row.get("finished_at")));
+            vo.setDetails(queryTaskDetails(taskNo));
+            return vo;
+        });
+    }
+
+    @Override
+    public List<TenantDictSyncResultVo> previewDiff(Long tenantId, List<String> dictTypes) {
+        TenantInfo tenant = contextHelper.loadActiveTenant(tenantId);
+        List<TenantDictTemplateType> templateTypes = resolveTemplateTypes(dictTypes);
+        TenantDictSyncResultVo result = previewOneTenant(tenant, templateTypes, TenantDictConstants.SYNC_MERGE);
+        return Collections.singletonList(result);
+    }
+
+    private void executeSyncTask(String taskNo, TenantDictSyncRunReq req, List<TenantInfo> tenants,
+                                 List<TenantDictTemplateType> templateTypes) {
+        int success = 0;
+        int failed = 0;
+        Map<String, List<TenantDictTemplateData>> dataCache = loadTemplateDataCache(templateTypes);
+
+        if (Boolean.TRUE.equals(req.getParallel())) {
+            int threads = req.getThreads() == null || req.getThreads() <= 0 ? 4 : req.getThreads();
+            ExecutorService pool = Executors.newFixedThreadPool(threads);
+            try {
+                List<Future<Boolean>> futures = new ArrayList<>();
+                for (TenantInfo tenant : tenants) {
+                    futures.add(pool.submit(() -> syncOneTenant(taskNo, tenant, templateTypes, dataCache, req.getSyncMode())));
+                }
+                for (Future<Boolean> f : futures) {
+                    try {
+                        if (Boolean.TRUE.equals(f.get())) success++;
+                        else failed++;
+                    } catch (Exception e) {
+                        failed++;
+                        log.error("[TenantDictSync] 并行任务异常", e);
+                    }
+                }
+            } finally {
+                pool.shutdown();
+            }
+        } else {
+            for (TenantInfo tenant : tenants) {
+                if (syncOneTenant(taskNo, tenant, templateTypes, dataCache, req.getSyncMode())) {
+                    success++;
+                } else {
+                    failed++;
+                }
+            }
+        }
+        finalizeTask(taskNo, success, failed);
+    }
+
+    private boolean syncOneTenant(String taskNo, TenantInfo tenant, List<TenantDictTemplateType> templateTypes,
+                                  Map<String, List<TenantDictTemplateData>> dataCache, String syncMode) {
+        Date start = new Date();
+        TenantDictSyncResultVo stats = new TenantDictSyncResultVo();
+        stats.setTenantId(tenant.getId());
+        stats.setTenantCode(tenant.getTenantCode());
+        stats.setTenantName(tenant.getTenantName());
+        String status = TenantDictConstants.DETAIL_SUCCESS;
+        String errorMsg = null;
+        try {
+            contextHelper.executeInTenant(tenant.getId(), () -> {
+                for (TenantDictTemplateType templateType : templateTypes) {
+                    String mode = resolveMode(syncMode, templateType);
+                    syncType(templateType, mode, stats);
+                    List<TenantDictTemplateData> templateDataList = dataCache.getOrDefault(templateType.getDictType(), Collections.emptyList());
+                    if (TenantDictConstants.SYNC_OVERWRITE.equals(mode)) {
+                        overwriteData(templateType.getDictType(), templateDataList, stats);
+                    } else {
+                        for (TenantDictTemplateData templateData : templateDataList) {
+                            syncData(templateData, mode, stats);
+                        }
+                    }
+                }
+                dictTypeService.resetDictCache();
+                return null;
+            });
+        } catch (Exception e) {
+            status = TenantDictConstants.DETAIL_FAILED;
+            errorMsg = e.getMessage();
+            log.error("[TenantDictSync] tenantId={} 同步失败", tenant.getId(), e);
+        }
+        insertTaskDetail(taskNo, tenant, stats, status, errorMsg, start, new Date());
+        return TenantDictConstants.DETAIL_SUCCESS.equals(status);
+    }
+
+    private TenantDictSyncResultVo previewOneTenant(TenantInfo tenant, List<TenantDictTemplateType> templateTypes, String syncMode) {
+        Map<String, List<TenantDictTemplateData>> dataCache = loadTemplateDataCache(templateTypes);
+        TenantDictSyncResultVo stats = new TenantDictSyncResultVo();
+        stats.setTenantId(tenant.getId());
+        stats.setTenantCode(tenant.getTenantCode());
+        stats.setTenantName(tenant.getTenantName());
+        contextHelper.executeInTenant(tenant.getId(), () -> {
+            for (TenantDictTemplateType templateType : templateTypes) {
+                String mode = resolveMode(syncMode, templateType);
+                previewType(templateType, mode, stats);
+                List<TenantDictTemplateData> templateDataList = dataCache.getOrDefault(templateType.getDictType(), Collections.emptyList());
+                if (TenantDictConstants.SYNC_OVERWRITE.equals(mode)) {
+                    previewOverwriteData(templateType.getDictType(), templateDataList, stats);
+                } else {
+                    for (TenantDictTemplateData templateData : templateDataList) {
+                        previewData(templateData, mode, stats);
+                    }
+                }
+            }
+            return null;
+        });
+        stats.setMessage("预览完成(未写入)");
+        return stats;
+    }
+
+    private void syncType(TenantDictTemplateType template, String mode, TenantDictSyncResultVo stats) {
+        SysDictType existing = dictTypeMapper.selectDictTypeByType(template.getDictType());
+        if (existing == null) {
+            SysDictType row = buildTypeFromTemplate(template);
+            dictTypeMapper.insertDictType(row);
+            stats.setTypeAdded(stats.getTypeAdded() + 1);
+            return;
+        }
+        if (TenantDictConstants.SYNC_APPEND.equals(mode)) {
+            stats.setDataSkipped(stats.getDataSkipped() + 1);
+            return;
+        }
+        if (canUpdateType(existing, mode)) {
+            applyTypeFromTemplate(existing, template);
+            dictTypeMapper.updateDictType(existing);
+            stats.setTypeUpdated(stats.getTypeUpdated() + 1);
+        } else {
+            stats.setDataSkipped(stats.getDataSkipped() + 1);
+        }
+    }
+
+    private void previewType(TenantDictTemplateType template, String mode, TenantDictSyncResultVo stats) {
+        SysDictType existing = dictTypeMapper.selectDictTypeByType(template.getDictType());
+        if (existing == null) {
+            stats.setTypeAdded(stats.getTypeAdded() + 1);
+        } else if (!TenantDictConstants.SYNC_APPEND.equals(mode) && canUpdateType(existing, mode)) {
+            stats.setTypeUpdated(stats.getTypeUpdated() + 1);
+        } else {
+            stats.setDataSkipped(stats.getDataSkipped() + 1);
+        }
+    }
+
+    private void syncData(TenantDictTemplateData template, String mode, TenantDictSyncResultVo stats) {
+        SysDictData existing = dictDataMapper.selectDictDataByTypeAndValue(template.getDictType(), template.getDictValue());
+        if (existing == null) {
+            dictDataMapper.insertDictData(buildDataFromTemplate(template));
+            stats.setDataAdded(stats.getDataAdded() + 1);
+            return;
+        }
+        if (TenantDictConstants.SYNC_APPEND.equals(mode)) {
+            stats.setDataSkipped(stats.getDataSkipped() + 1);
+            return;
+        }
+        if (canUpdateData(existing, template, mode)) {
+            applyDataFromTemplate(existing, template);
+            dictDataMapper.updateDictData(existing);
+            stats.setDataUpdated(stats.getDataUpdated() + 1);
+        } else {
+            stats.setDataSkipped(stats.getDataSkipped() + 1);
+        }
+    }
+
+    private void previewData(TenantDictTemplateData template, String mode, TenantDictSyncResultVo stats) {
+        SysDictData existing = dictDataMapper.selectDictDataByTypeAndValue(template.getDictType(), template.getDictValue());
+        if (existing == null) {
+            stats.setDataAdded(stats.getDataAdded() + 1);
+        } else if (!TenantDictConstants.SYNC_APPEND.equals(mode) && canUpdateData(existing, template, mode)) {
+            stats.setDataUpdated(stats.getDataUpdated() + 1);
+        } else {
+            stats.setDataSkipped(stats.getDataSkipped() + 1);
+        }
+    }
+
+    private void overwriteData(String dictType, List<TenantDictTemplateData> templateDataList, TenantDictSyncResultVo stats) {
+        List<SysDictData> tenantDataList = dictDataMapper.selectDictDataList(buildDataQuery(dictType));
+        Set<String> templateValues = templateDataList.stream().map(TenantDictTemplateData::getDictValue).collect(Collectors.toSet());
+        for (SysDictData row : tenantDataList) {
+            boolean platformItem = TenantDictConstants.SOURCE_PLATFORM.equals(row.getDictSource())
+                    || (row.getIsPlatformManaged() != null && row.getIsPlatformManaged() == 1);
+            if (platformItem && !templateValues.contains(row.getDictValue())) {
+                dictDataMapper.deleteDictDataById(row.getDictCode());
+                stats.setDataRemoved(stats.getDataRemoved() + 1);
+            }
+        }
+        for (TenantDictTemplateData template : templateDataList) {
+            SysDictData existing = dictDataMapper.selectDictDataByTypeAndValue(dictType, template.getDictValue());
+            if (existing == null) {
+                dictDataMapper.insertDictData(buildDataFromTemplate(template));
+                stats.setDataAdded(stats.getDataAdded() + 1);
+            } else if (canUpdateData(existing, template, TenantDictConstants.SYNC_OVERWRITE)
+                    || TenantDictConstants.SOURCE_PLATFORM.equals(existing.getDictSource())) {
+                applyDataFromTemplate(existing, template);
+                dictDataMapper.updateDictData(existing);
+                stats.setDataUpdated(stats.getDataUpdated() + 1);
+            } else {
+                stats.setDataSkipped(stats.getDataSkipped() + 1);
+            }
+        }
+    }
+
+    private void previewOverwriteData(String dictType, List<TenantDictTemplateData> templateDataList, TenantDictSyncResultVo stats) {
+        List<SysDictData> tenantDataList = dictDataMapper.selectDictDataList(buildDataQuery(dictType));
+        Set<String> templateValues = templateDataList.stream().map(TenantDictTemplateData::getDictValue).collect(Collectors.toSet());
+        for (SysDictData row : tenantDataList) {
+            boolean platformItem = TenantDictConstants.SOURCE_PLATFORM.equals(row.getDictSource())
+                    || (row.getIsPlatformManaged() != null && row.getIsPlatformManaged() == 1);
+            if (platformItem && !templateValues.contains(row.getDictValue())) {
+                stats.setDataRemoved(stats.getDataRemoved() + 1);
+            }
+        }
+        for (TenantDictTemplateData template : templateDataList) {
+            SysDictData existing = dictDataMapper.selectDictDataByTypeAndValue(dictType, template.getDictValue());
+            if (existing == null) {
+                stats.setDataAdded(stats.getDataAdded() + 1);
+            } else if (canUpdateData(existing, template, TenantDictConstants.SYNC_OVERWRITE)
+                    || TenantDictConstants.SOURCE_PLATFORM.equals(existing.getDictSource())) {
+                stats.setDataUpdated(stats.getDataUpdated() + 1);
+            } else {
+                stats.setDataSkipped(stats.getDataSkipped() + 1);
+            }
+        }
+    }
+
+    private boolean canUpdateType(SysDictType existing, String mode) {
+        if (TenantDictConstants.SYNC_OVERWRITE.equals(mode)) {
+            return true;
+        }
+        return TenantDictConstants.SOURCE_PLATFORM.equals(existing.getDictSource())
+                || (existing.getIsPlatformManaged() != null && existing.getIsPlatformManaged() == 1);
+    }
+
+    private boolean canUpdateData(SysDictData existing, TenantDictTemplateData template, String mode) {
+        if (TenantDictConstants.SYNC_OVERWRITE.equals(mode)) {
+            return TenantDictConstants.SOURCE_PLATFORM.equals(existing.getDictSource())
+                    || (existing.getIsPlatformManaged() != null && existing.getIsPlatformManaged() == 1)
+                    || (template.getIsManaged() != null && template.getIsManaged() == 1);
+        }
+        if (TenantDictConstants.SYNC_MERGE.equals(mode)) {
+            return TenantDictConstants.SOURCE_PLATFORM.equals(existing.getDictSource())
+                    || (existing.getIsPlatformManaged() != null && existing.getIsPlatformManaged() == 1)
+                    || (template.getIsManaged() != null && template.getIsManaged() == 1);
+        }
+        return false;
+    }
+
+    private SysDictType buildTypeFromTemplate(TenantDictTemplateType template) {
+        SysDictType row = new SysDictType();
+        applyTypeFromTemplate(row, template);
+        row.setCreateBy("platform-sync");
+        return row;
+    }
+
+    private void applyTypeFromTemplate(SysDictType row, TenantDictTemplateType template) {
+        row.setDictName(template.getDictName());
+        row.setDictType(template.getDictType());
+        row.setStatus(template.getStatus());
+        row.setRemark(template.getRemark());
+        row.setDictSource(TenantDictConstants.SOURCE_PLATFORM);
+        row.setIsPlatformManaged(template.getIsManaged() == null ? 1 : template.getIsManaged());
+        row.setUpdateBy("platform-sync");
+    }
+
+    private SysDictData buildDataFromTemplate(TenantDictTemplateData template) {
+        SysDictData row = new SysDictData();
+        applyDataFromTemplate(row, template);
+        row.setCreateBy("platform-sync");
+        return row;
+    }
+
+    private void applyDataFromTemplate(SysDictData row, TenantDictTemplateData template) {
+        row.setDictSort(template.getDictSort());
+        row.setDictLabel(template.getDictLabel());
+        row.setDictValue(template.getDictValue());
+        row.setDictType(template.getDictType());
+        row.setCssClass(template.getCssClass());
+        row.setListClass(template.getListClass());
+        row.setIsDefault(template.getIsDefault());
+        row.setStatus(template.getStatus());
+        row.setRemark(template.getRemark());
+        row.setDictSource(TenantDictConstants.SOURCE_PLATFORM);
+        row.setIsPlatformManaged(template.getIsManaged() == null ? 1 : template.getIsManaged());
+        row.setUpdateBy("platform-sync");
+    }
+
+    private SysDictData buildDataQuery(String dictType) {
+        SysDictData query = new SysDictData();
+        query.setDictType(dictType);
+        return query;
+    }
+
+    private String resolveMode(String globalMode, TenantDictTemplateType template) {
+        if (StringUtils.isNotEmpty(globalMode)) {
+            return globalMode.toUpperCase();
+        }
+        return template.getSyncMode() == null ? TenantDictConstants.SYNC_MERGE : template.getSyncMode().toUpperCase();
+    }
+
+    private Map<String, List<TenantDictTemplateData>> loadTemplateDataCache(List<TenantDictTemplateType> templateTypes) {
+        Map<String, List<TenantDictTemplateData>> cache = new HashMap<>();
+        for (TenantDictTemplateType type : templateTypes) {
+            cache.put(type.getDictType(), templateService.selectTemplateDataByType(type.getDictType()));
+        }
+        return cache;
+    }
+
+    private List<TenantDictTemplateType> resolveTemplateTypes(List<String> dictTypes) {
+        List<TenantDictTemplateType> all = templateService.selectAllTemplateTypes();
+        if (dictTypes == null || dictTypes.isEmpty()) {
+            return all;
+        }
+        Set<String> filter = dictTypes.stream().map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toSet());
+        return all.stream().filter(t -> filter.contains(t.getDictType())).collect(Collectors.toList());
+    }
+
+    private List<TenantInfo> resolveTenants(TenantDictSyncRunReq req) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        try {
+            List<TenantInfo> all = tenantInfoService.selectTenantInfoList(new TenantInfo());
+            all.removeIf(t -> t.getStatus() == null || t.getStatus() != 1);
+            String scope = req.getScopeType() == null ? TenantDictConstants.SCOPE_ALL : req.getScopeType().toUpperCase();
+            List<Long> ids = req.getTenantIds() == null ? Collections.emptyList() : req.getTenantIds();
+            if (TenantDictConstants.SCOPE_INCLUDE.equals(scope)) {
+                return all.stream().filter(t -> ids.contains(t.getId())).collect(Collectors.toList());
+            }
+            if (TenantDictConstants.SCOPE_EXCLUDE.equals(scope)) {
+                return all.stream().filter(t -> !ids.contains(t.getId())).collect(Collectors.toList());
+            }
+            return all;
+        } finally {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        }
+    }
+
+    private void validateSyncReq(TenantDictSyncRunReq req) {
+        if (req == null) {
+            throw new CustomException("同步参数不能为空");
+        }
+        String mode = req.getSyncMode() == null ? TenantDictConstants.SYNC_MERGE : req.getSyncMode().toUpperCase();
+        req.setSyncMode(mode);
+        if (!TenantDictConstants.SYNC_APPEND.equals(mode)
+                && !TenantDictConstants.SYNC_MERGE.equals(mode)
+                && !TenantDictConstants.SYNC_OVERWRITE.equals(mode)) {
+            throw new CustomException("不支持的同步模式: " + mode);
+        }
+        if (TenantDictConstants.SYNC_OVERWRITE.equals(mode) && !Boolean.TRUE.equals(req.getOverwriteConfirm())) {
+            throw new CustomException("OVERWRITE 模式风险较高,请设置 overwriteConfirm=true 后重试");
+        }
+    }
+
+    private String buildTaskNo() {
+        return "DSYNC" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
+                + String.format("%04d", new Random().nextInt(10000));
+    }
+
+    private void initMasterTaskTables() {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tenant_dict_sync_task (" +
+                "id bigint NOT NULL AUTO_INCREMENT, task_no varchar(32) NOT NULL, sync_mode varchar(20) NOT NULL, " +
+                "scope_type varchar(20) NOT NULL, dict_types text, total_tenants int NOT NULL DEFAULT 0, " +
+                "success_tenants int NOT NULL DEFAULT 0, failed_tenants int NOT NULL DEFAULT 0, status varchar(20) NOT NULL DEFAULT 'RUNNING', " +
+                "trigger_by varchar(64) DEFAULT NULL, started_at datetime DEFAULT NULL, finished_at datetime DEFAULT NULL, remark varchar(500) DEFAULT NULL, " +
+                "PRIMARY KEY (id), UNIQUE KEY uk_task_no (task_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tenant_dict_sync_task_detail (" +
+                "id bigint NOT NULL AUTO_INCREMENT, task_no varchar(32) NOT NULL, tenant_id bigint NOT NULL, tenant_code varchar(64) DEFAULT NULL, " +
+                "tenant_name varchar(128) DEFAULT NULL, status varchar(20) NOT NULL, type_added int DEFAULT 0, type_updated int DEFAULT 0, " +
+                "data_added int DEFAULT 0, data_updated int DEFAULT 0, data_skipped int DEFAULT 0, error_msg text, " +
+                "started_at datetime DEFAULT NULL, finished_at datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_task_no (task_no)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+    }
+
+    private void insertTask(String taskNo, TenantDictSyncRunReq req, int total, String triggerBy) {
+        contextHelper.runInMaster(() -> jdbcTemplate.update(
+                "INSERT INTO tenant_dict_sync_task(task_no,sync_mode,scope_type,dict_types,total_tenants,status,trigger_by,started_at) VALUES(?,?,?,?,?,?,?,NOW())",
+                taskNo, req.getSyncMode(), req.getScopeType(), JSON.toJSONString(req.getDictTypes()), total,
+                TenantDictConstants.TASK_RUNNING, triggerBy));
+    }
+
+    private void insertTaskDetail(String taskNo, TenantInfo tenant, TenantDictSyncResultVo stats,
+                                    String status, String errorMsg, Date start, Date end) {
+        contextHelper.runInMaster(() -> jdbcTemplate.update(
+                "INSERT INTO tenant_dict_sync_task_detail(task_no,tenant_id,tenant_code,tenant_name,status,type_added,type_updated,data_added,data_updated,data_skipped,error_msg,started_at,finished_at) " +
+                        "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)",
+                taskNo, tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), status,
+                stats.getTypeAdded(), stats.getTypeUpdated(), stats.getDataAdded(), stats.getDataUpdated(),
+                stats.getDataSkipped(), errorMsg, start, end));
+    }
+
+    private void finalizeTask(String taskNo, int success, int failed) {
+        contextHelper.runInMaster(() -> {
+            String status = failed == 0 ? TenantDictConstants.TASK_SUCCESS
+                    : (success == 0 ? TenantDictConstants.TASK_FAILED : TenantDictConstants.TASK_PARTIAL);
+            jdbcTemplate.update(
+                    "UPDATE tenant_dict_sync_task SET success_tenants=?, failed_tenants=?, status=?, finished_at=NOW() WHERE task_no=?",
+                    success, failed, status, taskNo);
+        });
+    }
+
+    private List<TenantDictSyncTaskDetailVo> queryTaskDetails(String taskNo) {
+        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+                "SELECT tenant_id,tenant_code,tenant_name,status,type_added,type_updated,data_added,data_updated,data_skipped,error_msg,started_at,finished_at " +
+                        "FROM tenant_dict_sync_task_detail WHERE task_no=? ORDER BY id ASC", taskNo);
+        List<TenantDictSyncTaskDetailVo> list = new ArrayList<>();
+        for (Map<String, Object> row : rows) {
+            TenantDictSyncTaskDetailVo vo = new TenantDictSyncTaskDetailVo();
+            vo.setTenantId(longVal(row.get("tenant_id")));
+            vo.setTenantCode(str(row.get("tenant_code")));
+            vo.setTenantName(str(row.get("tenant_name")));
+            vo.setStatus(str(row.get("status")));
+            vo.setTypeAdded(intVal(row.get("type_added")));
+            vo.setTypeUpdated(intVal(row.get("type_updated")));
+            vo.setDataAdded(intVal(row.get("data_added")));
+            vo.setDataUpdated(intVal(row.get("data_updated")));
+            vo.setDataSkipped(intVal(row.get("data_skipped")));
+            vo.setErrorMsg(str(row.get("error_msg")));
+            vo.setStartedAt(str(row.get("started_at")));
+            vo.setFinishedAt(str(row.get("finished_at")));
+            list.add(vo);
+        }
+        return list;
+    }
+
+    private String str(Object o) {
+        return o == null ? null : String.valueOf(o);
+    }
+
+    private int intVal(Object o) {
+        return o == null ? 0 : Integer.parseInt(String.valueOf(o));
+    }
+
+    private long longVal(Object o) {
+        return o == null ? 0L : Long.parseLong(String.valueOf(o));
+    }
+}

+ 325 - 0
fs-service/src/main/java/com/fs/tenant/dict/service/impl/TenantDictTemplateServiceImpl.java

@@ -0,0 +1,325 @@
+package com.fs.tenant.dict.service.impl;
+
+import com.fs.common.constant.UserConstants;
+import com.fs.common.exception.CustomException;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.core.domain.entity.SysDictType;
+import com.fs.tenant.dict.dto.TenantDictTemplateImportReq;
+import com.fs.tenant.dict.service.TenantDictManageService;
+import com.fs.tenant.dict.vo.TenantDictTemplateImportResultVo;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.system.service.ISysDictTypeService;
+import org.springframework.util.CollectionUtils;
+import com.fs.tenant.dict.domain.TenantDictTemplateData;
+import com.fs.tenant.dict.domain.TenantDictTemplateType;
+import com.fs.tenant.dict.helper.TenantDictContextHelper;
+import com.fs.tenant.dict.mapper.TenantDictTemplateMapper;
+import com.fs.tenant.dict.service.TenantDictTemplateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class TenantDictTemplateServiceImpl implements TenantDictTemplateService {
+
+    @Autowired
+    private TenantDictTemplateMapper templateMapper;
+
+    @Autowired
+    private TenantDictContextHelper contextHelper;
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    private ISysDictTypeService sysDictTypeService;
+
+    @Autowired
+    private ISysDictDataService sysDictDataService;
+
+    @Autowired
+    private TenantDictManageService tenantDictManageService;
+
+    @Override
+    public List<TenantDictTemplateType> selectTemplateTypeList(TenantDictTemplateType query) {
+        return contextHelper.executeInMaster(() -> {
+            ensureTemplateTables();
+            return templateMapper.selectTemplateTypeList(query);
+        });
+    }
+
+    @Override
+    public TenantDictTemplateType selectTemplateTypeById(Long dictId) {
+        return contextHelper.executeInMaster(() -> templateMapper.selectTemplateTypeById(dictId));
+    }
+
+    @Override
+    public int insertTemplateType(TenantDictTemplateType row) {
+        normalizeType(row, true);
+        return contextHelper.executeInMaster(() -> {
+            ensureTemplateTables();
+            if (templateMapper.selectTemplateTypeByType(row.getDictType()) != null) {
+                throw new CustomException("模板字典类型已存在: " + row.getDictType());
+            }
+            return templateMapper.insertTemplateType(row);
+        });
+    }
+
+    @Override
+    public int updateTemplateType(TenantDictTemplateType row) {
+        normalizeType(row, false);
+        return contextHelper.executeInMaster(() -> templateMapper.updateTemplateType(row));
+    }
+
+    @Override
+    public void deleteTemplateTypeByIds(Long[] dictIds) {
+        contextHelper.runInMaster(() -> {
+            for (Long dictId : dictIds) {
+                TenantDictTemplateType type = templateMapper.selectTemplateTypeById(dictId);
+                if (type != null && templateMapper.countTemplateDataByType(type.getDictType()) > 0) {
+                    throw new CustomException(type.getDictName() + " 下存在模板数据,不能删除");
+                }
+            }
+            templateMapper.deleteTemplateTypeByIds(dictIds);
+        });
+    }
+
+    @Override
+    public List<TenantDictTemplateData> selectTemplateDataList(TenantDictTemplateData query) {
+        return contextHelper.executeInMaster(() -> templateMapper.selectTemplateDataList(query));
+    }
+
+    @Override
+    public TenantDictTemplateData selectTemplateDataById(Long dictCode) {
+        return contextHelper.executeInMaster(() -> templateMapper.selectTemplateDataById(dictCode));
+    }
+
+    @Override
+    public int insertTemplateData(TenantDictTemplateData row) {
+        normalizeData(row);
+        return contextHelper.executeInMaster(() -> {
+            if (templateMapper.selectTemplateTypeByType(row.getDictType()) == null) {
+                throw new CustomException("模板字典类型不存在,请先创建类型: " + row.getDictType());
+            }
+            if (templateMapper.selectTemplateDataByTypeAndValue(row.getDictType(), row.getDictValue()) != null) {
+                throw new CustomException("同类型下键值已存在: " + row.getDictValue());
+            }
+            return templateMapper.insertTemplateData(row);
+        });
+    }
+
+    @Override
+    public int updateTemplateData(TenantDictTemplateData row) {
+        normalizeData(row);
+        return contextHelper.executeInMaster(() -> templateMapper.updateTemplateData(row));
+    }
+
+    @Override
+    public void deleteTemplateDataByIds(Long[] dictCodes) {
+        contextHelper.runInMaster(() -> templateMapper.deleteTemplateDataByIds(dictCodes));
+    }
+
+    @Override
+    public List<TenantDictTemplateType> selectAllTemplateTypes() {
+        return contextHelper.executeInMaster(() -> templateMapper.selectTemplateTypeList(new TenantDictTemplateType()));
+    }
+
+    @Override
+    public List<TenantDictTemplateData> selectTemplateDataByType(String dictType) {
+        return contextHelper.executeInMaster(() -> templateMapper.selectTemplateDataByType(dictType));
+    }
+
+    @Override
+    public TenantDictTemplateImportResultVo importFromPlatform(TenantDictTemplateImportReq req, String operator) {
+        boolean overwrite = Boolean.TRUE.equals(req.getOverwriteExisting());
+        List<SysDictType> sourceTypes = resolvePlatformSourceTypes(req);
+        if (sourceTypes.isEmpty()) {
+            throw new CustomException("未找到可导入的平台字典类型");
+        }
+        TenantDictTemplateImportResultVo result = new TenantDictTemplateImportResultVo();
+        contextHelper.runInMaster(() -> {
+            ensureTemplateTables();
+            for (SysDictType sourceType : sourceTypes) {
+                importOneTypeAndData(
+                        toTemplateType(sourceType, operator),
+                        loadPlatformData(sourceType.getDictType()),
+                        overwrite,
+                        result,
+                        operator
+                );
+            }
+        });
+        result.setMessage(String.format("导入完成:类型+%d 更新%d 跳过%d;数据+%d 更新%d 跳过%d",
+                result.getTypeAdded(), result.getTypeUpdated(), result.getTypeSkipped(),
+                result.getDataAdded(), result.getDataUpdated(), result.getDataSkipped()));
+        return result;
+    }
+
+    @Override
+    public TenantDictTemplateImportResultVo importFromTenant(TenantDictTemplateImportReq req, String operator) {
+        if (req.getTenantId() == null) {
+            throw new CustomException("请选择租户");
+        }
+        if (CollectionUtils.isEmpty(req.getDictTypes())) {
+            throw new CustomException("请选择要导入的字典类型");
+        }
+        boolean overwrite = Boolean.TRUE.equals(req.getOverwriteExisting());
+        TenantDictTemplateImportResultVo result = new TenantDictTemplateImportResultVo();
+        for (String dictType : req.getDictTypes()) {
+            SysDictType query = new SysDictType();
+            query.setDictType(dictType);
+            List<SysDictType> types = tenantDictManageService.selectDictTypeList(req.getTenantId(), query);
+            if (types.isEmpty()) {
+                result.setTypeSkipped(result.getTypeSkipped() + 1);
+                continue;
+            }
+            SysDictType sourceType = types.get(0);
+            SysDictData dataQuery = new SysDictData();
+            dataQuery.setDictType(dictType);
+            List<SysDictData> dataList = tenantDictManageService.selectDictDataList(req.getTenantId(), dataQuery);
+            contextHelper.runInMaster(() -> {
+                ensureTemplateTables();
+                importOneTypeAndData(toTemplateType(sourceType, operator), dataList, overwrite, result, operator);
+            });
+        }
+        result.setMessage(String.format("导入完成:类型+%d 更新%d 跳过%d;数据+%d 更新%d 跳过%d",
+                result.getTypeAdded(), result.getTypeUpdated(), result.getTypeSkipped(),
+                result.getDataAdded(), result.getDataUpdated(), result.getDataSkipped()));
+        return result;
+    }
+
+    private List<SysDictType> resolvePlatformSourceTypes(TenantDictTemplateImportReq req) {
+        return contextHelper.executeInMaster(() -> {
+            if (!CollectionUtils.isEmpty(req.getDictIds())) {
+                List<SysDictType> list = new java.util.ArrayList<>();
+                for (Long dictId : req.getDictIds()) {
+                    SysDictType row = sysDictTypeService.selectDictTypeById(dictId);
+                    if (row != null) {
+                        list.add(row);
+                    }
+                }
+                return list;
+            }
+            if (!CollectionUtils.isEmpty(req.getDictTypes())) {
+                List<SysDictType> list = new java.util.ArrayList<>();
+                for (String dictType : req.getDictTypes()) {
+                    SysDictType row = sysDictTypeService.selectDictTypeByType(dictType);
+                    if (row != null) {
+                        list.add(row);
+                    }
+                }
+                return list;
+            }
+            return sysDictTypeService.selectDictTypeList(new SysDictType());
+        });
+    }
+
+    private List<SysDictData> loadPlatformData(String dictType) {
+        SysDictData query = new SysDictData();
+        query.setDictType(dictType);
+        return sysDictDataService.selectDictDataList(query);
+    }
+
+    private TenantDictTemplateType toTemplateType(SysDictType source, String operator) {
+        TenantDictTemplateType row = new TenantDictTemplateType();
+        row.setDictName(source.getDictName());
+        row.setDictType(source.getDictType());
+        row.setStatus(source.getStatus());
+        row.setRemark(source.getRemark());
+        row.setIsManaged(1);
+        row.setSyncMode("MERGE");
+        row.setCreateBy(operator);
+        row.setUpdateBy(operator);
+        return row;
+    }
+
+    private TenantDictTemplateData toTemplateData(SysDictData source, String operator) {
+        TenantDictTemplateData row = new TenantDictTemplateData();
+        row.setDictSort(source.getDictSort() == null ? 0L : source.getDictSort());
+        row.setDictLabel(source.getDictLabel());
+        row.setDictValue(source.getDictValue());
+        row.setDictType(source.getDictType());
+        row.setCssClass(source.getCssClass());
+        row.setListClass(source.getListClass());
+        row.setIsDefault(source.getIsDefault());
+        row.setStatus(source.getStatus());
+        row.setRemark(source.getRemark());
+        row.setIsManaged(1);
+        row.setCreateBy(operator);
+        row.setUpdateBy(operator);
+        return row;
+    }
+
+    private void importOneTypeAndData(TenantDictTemplateType typeRow, List<SysDictData> dataList,
+                                      boolean overwrite, TenantDictTemplateImportResultVo result, String operator) {
+        TenantDictTemplateType existingType = templateMapper.selectTemplateTypeByType(typeRow.getDictType());
+        if (existingType == null) {
+            templateMapper.insertTemplateType(typeRow);
+            result.setTypeAdded(result.getTypeAdded() + 1);
+        } else if (overwrite) {
+            typeRow.setDictId(existingType.getDictId());
+            templateMapper.updateTemplateType(typeRow);
+            result.setTypeUpdated(result.getTypeUpdated() + 1);
+        } else {
+            result.setTypeSkipped(result.getTypeSkipped() + 1);
+        }
+        for (SysDictData sourceData : dataList) {
+            TenantDictTemplateData dataRow = toTemplateData(sourceData, operator);
+            TenantDictTemplateData existingData = templateMapper.selectTemplateDataByTypeAndValue(
+                    dataRow.getDictType(), dataRow.getDictValue());
+            if (existingData == null) {
+                templateMapper.insertTemplateData(dataRow);
+                result.setDataAdded(result.getDataAdded() + 1);
+            } else if (overwrite) {
+                dataRow.setDictCode(existingData.getDictCode());
+                templateMapper.updateTemplateData(dataRow);
+                result.setDataUpdated(result.getDataUpdated() + 1);
+            } else {
+                result.setDataSkipped(result.getDataSkipped() + 1);
+            }
+        }
+    }
+
+    private void normalizeType(TenantDictTemplateType row, boolean isCreate) {
+        if (row.getStatus() == null) {
+            row.setStatus(UserConstants.NORMAL);
+        }
+        if (row.getIsManaged() == null) {
+            row.setIsManaged(1);
+        }
+        if (row.getSyncMode() == null || row.getSyncMode().isEmpty()) {
+            row.setSyncMode("MERGE");
+        }
+    }
+
+    private void normalizeData(TenantDictTemplateData row) {
+        if (row.getStatus() == null) {
+            row.setStatus(UserConstants.NORMAL);
+        }
+        if (row.getIsManaged() == null) {
+            row.setIsManaged(1);
+        }
+        if (row.getIsDefault() == null) {
+            row.setIsDefault("N");
+        }
+        if (row.getDictSort() == null) {
+            row.setDictSort(0L);
+        }
+    }
+
+    private void ensureTemplateTables() {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tenant_dict_template_type (" +
+                "dict_id bigint NOT NULL AUTO_INCREMENT, dict_name varchar(100) NOT NULL, dict_type varchar(100) NOT NULL, " +
+                "status char(1) NOT NULL DEFAULT '0', is_managed tinyint NOT NULL DEFAULT 1, sync_mode varchar(20) NOT NULL DEFAULT 'MERGE', " +
+                "remark varchar(500) DEFAULT NULL, create_by varchar(64) DEFAULT '', create_time datetime DEFAULT NULL, " +
+                "update_by varchar(64) DEFAULT '', update_time datetime DEFAULT NULL, PRIMARY KEY (dict_id), UNIQUE KEY uk_dict_type (dict_type)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tenant_dict_template_data (" +
+                "dict_code bigint NOT NULL AUTO_INCREMENT, dict_sort int DEFAULT 0, dict_label varchar(100) NOT NULL, dict_value varchar(100) NOT NULL, " +
+                "dict_type varchar(100) NOT NULL, css_class varchar(100) DEFAULT NULL, list_class varchar(100) DEFAULT NULL, is_default char(1) DEFAULT 'N', " +
+                "status char(1) NOT NULL DEFAULT '0', is_managed tinyint NOT NULL DEFAULT 1, remark varchar(500) DEFAULT NULL, " +
+                "create_by varchar(64) DEFAULT '', create_time datetime DEFAULT NULL, update_by varchar(64) DEFAULT '', update_time datetime DEFAULT NULL, " +
+                "PRIMARY KEY (dict_code), UNIQUE KEY uk_type_value (dict_type, dict_value), KEY idx_dict_type (dict_type)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+    }
+}

+ 21 - 0
fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictSyncResultVo.java

@@ -0,0 +1,21 @@
+package com.fs.tenant.dict.vo;
+
+import lombok.Data;
+
+/**
+ * 单租户同步统计(dryRun 或明细共用)
+ */
+@Data
+public class TenantDictSyncResultVo {
+
+    private Long tenantId;
+    private String tenantCode;
+    private String tenantName;
+    private int typeAdded;
+    private int typeUpdated;
+    private int dataAdded;
+    private int dataUpdated;
+    private int dataSkipped;
+    private int dataRemoved;
+    private String message;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictSyncTaskDetailVo.java

@@ -0,0 +1,20 @@
+package com.fs.tenant.dict.vo;
+
+import lombok.Data;
+
+@Data
+public class TenantDictSyncTaskDetailVo {
+
+    private Long tenantId;
+    private String tenantCode;
+    private String tenantName;
+    private String status;
+    private Integer typeAdded;
+    private Integer typeUpdated;
+    private Integer dataAdded;
+    private Integer dataUpdated;
+    private Integer dataSkipped;
+    private String errorMsg;
+    private String startedAt;
+    private String finishedAt;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictSyncTaskVo.java

@@ -0,0 +1,22 @@
+package com.fs.tenant.dict.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class TenantDictSyncTaskVo {
+
+    private String taskNo;
+    private String syncMode;
+    private String scopeType;
+    private String dictTypes;
+    private Integer totalTenants;
+    private Integer successTenants;
+    private Integer failedTenants;
+    private String status;
+    private String triggerBy;
+    private String startedAt;
+    private String finishedAt;
+    private List<TenantDictSyncTaskDetailVo> details;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/tenant/dict/vo/TenantDictTemplateImportResultVo.java

@@ -0,0 +1,18 @@
+package com.fs.tenant.dict.vo;
+
+import lombok.Data;
+
+/**
+ * 导入平台模板结果
+ */
+@Data
+public class TenantDictTemplateImportResultVo {
+
+    private int typeAdded;
+    private int typeUpdated;
+    private int typeSkipped;
+    private int dataAdded;
+    private int dataUpdated;
+    private int dataSkipped;
+    private String message;
+}

+ 6 - 0
fs-service/src/main/resources/db/tenant-initTable.sql

@@ -16341,6 +16341,8 @@ CREATE TABLE `sys_dict_data`
     `list_class`  varchar(100)   NULL DEFAULT NULL COMMENT '表格回显样式',
     `is_default`  char(1)   NULL DEFAULT 'N' COMMENT '是否默认(Y是 N否)',
     `status`      char(1)   NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
+    `dict_source` varchar(20) NULL DEFAULT 'tenant' COMMENT '来源 platform/tenant',
+    `is_platform_managed` tinyint NULL DEFAULT 0 COMMENT '是否平台管控 1是 0否',
     `create_by`   varchar(64)   NULL DEFAULT '' COMMENT '创建者',
     `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
     `update_by`   varchar(64)   NULL DEFAULT '' COMMENT '更新者',
@@ -16359,6 +16361,8 @@ CREATE TABLE `sys_dict_type`
     `dict_name`   varchar(100)   NULL DEFAULT '' COMMENT '字典名称',
     `dict_type`   varchar(100)   NULL DEFAULT '' COMMENT '字典类型',
     `status`      char(1)   NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
+    `dict_source` varchar(20) NULL DEFAULT 'tenant' COMMENT '来源 platform/tenant',
+    `is_platform_managed` tinyint NULL DEFAULT 0 COMMENT '是否平台管控 1是 0否',
     `create_by`   varchar(64)   NULL DEFAULT '' COMMENT '创建者',
     `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
     `update_by`   varchar(64)   NULL DEFAULT '' COMMENT '更新者',
@@ -16378,6 +16382,8 @@ CREATE TABLE `sys_dict_type_copy1`
     `dict_name`   varchar(100)   NULL DEFAULT '' COMMENT '字典名称',
     `dict_type`   varchar(100)   NULL DEFAULT '' COMMENT '字典类型',
     `status`      char(1)   NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
+    `dict_source` varchar(20) NULL DEFAULT 'tenant' COMMENT '来源 platform/tenant',
+    `is_platform_managed` tinyint NULL DEFAULT 0 COMMENT '是否平台管控 1是 0否',
     `create_by`   varchar(64)   NULL DEFAULT '' COMMENT '创建者',
     `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
     `update_by`   varchar(64)   NULL DEFAULT '' COMMENT '更新者',

+ 9 - 1
fs-service/src/main/resources/mapper/system/SysDictDataMapper.xml

@@ -14,6 +14,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<result property="listClass"  column="list_class"  />
 		<result property="isDefault"  column="is_default"  />
 		<result property="status"     column="status"      />
+		<result property="dictSource" column="dict_source" />
+		<result property="isPlatformManaged" column="is_platform_managed" />
 		<result property="createBy"   column="create_by"   />
 		<result property="createTime" column="create_time" />
 		<result property="updateBy"   column="update_by"   />
@@ -21,7 +23,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	</resultMap>
 	
 	<sql id="selectDictDataVo">
-        select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark 
+        select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, dict_source, is_platform_managed, create_by, create_time, remark 
 		from sys_dict_data
     </sql>
 
@@ -85,6 +87,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="listClass != null">list_class = #{listClass},</if>
  			<if test="isDefault != null and isDefault != ''">is_default = #{isDefault},</if>
  			<if test="status != null">status = #{status},</if>
+ 			<if test="dictSource != null and dictSource != ''">dict_source = #{dictSource},</if>
+ 			<if test="isPlatformManaged != null">is_platform_managed = #{isPlatformManaged},</if>
  			<if test="remark != null">remark = #{remark},</if>
  			<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
  			update_time = sysdate()
@@ -106,6 +110,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="listClass != null and listClass != ''">list_class,</if>
  			<if test="isDefault != null and isDefault != ''">is_default,</if>
  			<if test="status != null">status,</if>
+ 			<if test="dictSource != null and dictSource != ''">dict_source,</if>
+ 			<if test="isPlatformManaged != null">is_platform_managed,</if>
  			<if test="remark != null and remark != ''">remark,</if>
  			<if test="createBy != null and createBy != ''">create_by,</if>
  			create_time
@@ -118,6 +124,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="listClass != null and listClass != ''">#{listClass},</if>
  			<if test="isDefault != null and isDefault != ''">#{isDefault},</if>
  			<if test="status != null">#{status},</if>
+ 			<if test="dictSource != null and dictSource != ''">#{dictSource},</if>
+ 			<if test="isPlatformManaged != null">#{isPlatformManaged},</if>
  			<if test="remark != null and remark != ''">#{remark},</if>
  			<if test="createBy != null and createBy != ''">#{createBy},</if>
  			sysdate()

+ 9 - 1
fs-service/src/main/resources/mapper/system/SysDictTypeMapper.xml

@@ -9,6 +9,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<result property="dictName"   column="dict_name"   />
 		<result property="dictType"   column="dict_type"   />
 		<result property="status"     column="status"      />
+		<result property="dictSource" column="dict_source" />
+		<result property="isPlatformManaged" column="is_platform_managed" />
 		<result property="createBy"   column="create_by"   />
 		<result property="createTime" column="create_time" />
 		<result property="updateBy"   column="update_by"   />
@@ -16,7 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	</resultMap>
 	
 	<sql id="selectDictTypeVo">
-        select dict_id, dict_name, dict_type, status, create_by, create_time, remark 
+        select dict_id, dict_name, dict_type, status, dict_source, is_platform_managed, create_by, create_time, remark 
 		from sys_dict_type
     </sql>
 
@@ -77,6 +79,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="dictName != null and dictName != ''">dict_name = #{dictName},</if>
  			<if test="dictType != null and dictType != ''">dict_type = #{dictType},</if>
  			<if test="status != null">status = #{status},</if>
+ 			<if test="dictSource != null and dictSource != ''">dict_source = #{dictSource},</if>
+ 			<if test="isPlatformManaged != null">is_platform_managed = #{isPlatformManaged},</if>
  			<if test="remark != null">remark = #{remark},</if>
  			<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
  			update_time = sysdate()
@@ -89,6 +93,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="dictName != null and dictName != ''">dict_name,</if>
  			<if test="dictType != null and dictType != ''">dict_type,</if>
  			<if test="status != null">status,</if>
+ 			<if test="dictSource != null and dictSource != ''">dict_source,</if>
+ 			<if test="isPlatformManaged != null">is_platform_managed,</if>
  			<if test="remark != null and remark != ''">remark,</if>
  			<if test="createBy != null and createBy != ''">create_by,</if>
  			create_time
@@ -96,6 +102,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  			<if test="dictName != null and dictName != ''">#{dictName},</if>
  			<if test="dictType != null and dictType != ''">#{dictType},</if>
  			<if test="status != null">#{status},</if>
+ 			<if test="dictSource != null and dictSource != ''">#{dictSource},</if>
+ 			<if test="isPlatformManaged != null">#{isPlatformManaged},</if>
  			<if test="remark != null and remark != ''">#{remark},</if>
  			<if test="createBy != null and createBy != ''">#{createBy},</if>
  			sysdate()

+ 146 - 0
fs-service/src/main/resources/mapper/tenant/TenantDictTemplateMapper.xml

@@ -0,0 +1,146 @@
+<?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.fs.tenant.dict.mapper.TenantDictTemplateMapper">
+
+    <resultMap id="TemplateTypeResult" type="com.fs.tenant.dict.domain.TenantDictTemplateType">
+        <id property="dictId" column="dict_id"/>
+        <result property="dictName" column="dict_name"/>
+        <result property="dictType" column="dict_type"/>
+        <result property="status" column="status"/>
+        <result property="isManaged" column="is_managed"/>
+        <result property="syncMode" column="sync_mode"/>
+        <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>
+
+    <resultMap id="TemplateDataResult" type="com.fs.tenant.dict.domain.TenantDictTemplateData">
+        <id property="dictCode" column="dict_code"/>
+        <result property="dictSort" column="dict_sort"/>
+        <result property="dictLabel" column="dict_label"/>
+        <result property="dictValue" column="dict_value"/>
+        <result property="dictType" column="dict_type"/>
+        <result property="cssClass" column="css_class"/>
+        <result property="listClass" column="list_class"/>
+        <result property="isDefault" column="is_default"/>
+        <result property="status" column="status"/>
+        <result property="isManaged" column="is_managed"/>
+        <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="selectTemplateTypeVo">
+        select dict_id, dict_name, dict_type, status, is_managed, sync_mode, create_by, create_time, update_by, update_time, remark
+        from tenant_dict_template_type
+    </sql>
+
+    <sql id="selectTemplateDataVo">
+        select dict_code, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, is_managed,
+               create_by, create_time, update_by, update_time, remark
+        from tenant_dict_template_data
+    </sql>
+
+    <select id="selectTemplateTypeList" resultMap="TemplateTypeResult">
+        <include refid="selectTemplateTypeVo"/>
+        <where>
+            <if test="dictName != null and dictName != ''">and dict_name like concat('%', #{dictName}, '%')</if>
+            <if test="dictType != null and dictType != ''">and dict_type like concat('%', #{dictType}, '%')</if>
+            <if test="status != null and status != ''">and status = #{status}</if>
+        </where>
+        order by dict_id desc
+    </select>
+
+    <select id="selectTemplateTypeById" resultMap="TemplateTypeResult">
+        <include refid="selectTemplateTypeVo"/> where dict_id = #{dictId}
+    </select>
+
+    <select id="selectTemplateTypeByType" resultMap="TemplateTypeResult">
+        <include refid="selectTemplateTypeVo"/> where dict_type = #{dictType} limit 1
+    </select>
+
+    <insert id="insertTemplateType" useGeneratedKeys="true" keyProperty="dictId">
+        insert into tenant_dict_template_type(dict_name, dict_type, status, is_managed, sync_mode, remark, create_by, create_time)
+        values(#{dictName}, #{dictType}, #{status}, #{isManaged}, #{syncMode}, #{remark}, #{createBy}, sysdate())
+    </insert>
+
+    <update id="updateTemplateType">
+        update tenant_dict_template_type
+        <set>
+            <if test="dictName != null and dictName != ''">dict_name = #{dictName},</if>
+            <if test="dictType != null and dictType != ''">dict_type = #{dictType},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="isManaged != null">is_managed = #{isManaged},</if>
+            <if test="syncMode != null and syncMode != ''">sync_mode = #{syncMode},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate()
+        </set>
+        where dict_id = #{dictId}
+    </update>
+
+    <delete id="deleteTemplateTypeByIds">
+        delete from tenant_dict_template_type where dict_id in
+        <foreach collection="array" item="dictId" open="(" separator="," close=")">#{dictId}</foreach>
+    </delete>
+
+    <select id="selectTemplateDataList" resultMap="TemplateDataResult">
+        <include refid="selectTemplateDataVo"/>
+        <where>
+            <if test="dictType != null and dictType != ''">and dict_type = #{dictType}</if>
+            <if test="dictLabel != null and dictLabel != ''">and dict_label like concat('%', #{dictLabel}, '%')</if>
+            <if test="status != null and status != ''">and status = #{status}</if>
+        </where>
+        order by dict_sort asc, dict_code asc
+    </select>
+
+    <select id="selectTemplateDataById" resultMap="TemplateDataResult">
+        <include refid="selectTemplateDataVo"/> where dict_code = #{dictCode}
+    </select>
+
+    <select id="selectTemplateDataByTypeAndValue" resultMap="TemplateDataResult">
+        <include refid="selectTemplateDataVo"/>
+        where dict_type = #{dictType} and dict_value = #{dictValue} limit 1
+    </select>
+
+    <select id="selectTemplateDataByType" resultMap="TemplateDataResult">
+        <include refid="selectTemplateDataVo"/> where dict_type = #{dictType} order by dict_sort asc
+    </select>
+
+    <insert id="insertTemplateData" useGeneratedKeys="true" keyProperty="dictCode">
+        insert into tenant_dict_template_data(dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, is_managed, remark, create_by, create_time)
+        values(#{dictSort}, #{dictLabel}, #{dictValue}, #{dictType}, #{cssClass}, #{listClass}, #{isDefault}, #{status}, #{isManaged}, #{remark}, #{createBy}, sysdate())
+    </insert>
+
+    <update id="updateTemplateData">
+        update tenant_dict_template_data
+        <set>
+            <if test="dictSort != null">dict_sort = #{dictSort},</if>
+            <if test="dictLabel != null and dictLabel != ''">dict_label = #{dictLabel},</if>
+            <if test="dictValue != null and dictValue != ''">dict_value = #{dictValue},</if>
+            <if test="dictType != null and dictType != ''">dict_type = #{dictType},</if>
+            <if test="cssClass != null">css_class = #{cssClass},</if>
+            <if test="listClass != null">list_class = #{listClass},</if>
+            <if test="isDefault != null">is_default = #{isDefault},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="isManaged != null">is_managed = #{isManaged},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate()
+        </set>
+        where dict_code = #{dictCode}
+    </update>
+
+    <delete id="deleteTemplateDataByIds">
+        delete from tenant_dict_template_data where dict_code in
+        <foreach collection="array" item="dictCode" open="(" separator="," close=")">#{dictCode}</foreach>
+    </delete>
+
+    <select id="countTemplateDataByType" resultType="int">
+        select count(1) from tenant_dict_template_data where dict_type = #{dictType}
+    </select>
+</mapper>