下单系统寄/到件省市区关联选择功能实现方案

物流下单系统寄/到件省市区关联选择功能实现方案

一、需求背景与业务目标

在物流下单场景中,寄件地址与收件地址并非全域可选,需严格匹配后台维护的报价线路规则:

  1. 基础数据:行政区域表(省/市/区/街道四级树形结构)、报价维护表(维护始发市/区县 → 目的市/区县的可配送线路);
  2. 核心需求:用户选择寄件行政区域后,收件区域仅展示该寄件区域关联的可配送范围;反之选择收件区域后,寄件区域仅展示关联范围;
  3. 功能要求:支持省市区三级联动选择、地址模糊查询,保证查询性能与数据准确性。

二、整体技术方案设计

1. 设计思路

为避免实时查询行政区域+报价表导致的性能问题,采用**「预生成关联数据 + 定时全量刷新 + MQ异步处理」**方案:

  1. 全量拉取报价表的始发编码(srcCode)目的编码(destCode)
  2. 递归解析每个编码的上级区域(省/市)+ 下级区域(区/街道) ,构建完整的区域关联关系;
  3. 将关联数据预存储到专用表oms_district,前端查询直接命中预生成数据;
  4. 通过定时任务+MQ保证数据实时性,异步处理避免阻塞主线程。

2. 核心表结构设计

新建oms_district(下单行政区域关联表),存储寄件/目的区域的关联关系、行政区域全量信息:

less 复制代码
CREATE TABLE `oms_district` (
  `id` bigint NOT NULL COMMENT 'id',
  `code` varchar(10) COMMENT '行政区域编码',
  `parent_code` varchar(10) COMMENT '上级区域编码',
  `name` varchar(45) COMMENT '区域名称',
  `merger_code` varchar(255) COMMENT '行政区域code全称(逗号分隔)',
  `merger_name` varchar(200) COMMENT '全名称',
  `short_name` varchar(45) COMMENT '简称',
  `merger_short_name` varchar(200) COMMENT '全简称',
  `level_type` varchar(45) COMMENT '区域等级0-国家1-省2-市3-区县4-乡镇',
  `src_code` varchar(50) COMMENT '始发区域',
  `dest_code` varchar(50) COMMENT '目的区域',
  `type` varchar(45) COMMENT '0-始发;1-目的',
  `is_delete` int DEFAULT '0' COMMENT '是否删除',
  -- 通用审计字段(创建人/时间/版本/部门等)
  PRIMARY KEY (`id`),
  KEY `idx_district_code` (`code`),
  KEY `idx_query_index` (`type`,`parent_code`,`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='下单行政区域';

关键字段说明

  • type:区分数据类型(0=寄件/始发,1=收件/目的);
  • src_code/dest_code:存储关联的对端区域编码;
  • level_type:支持四级行政区域分级,实现三级联动。

三、核心业务流程

复制代码
定时任务触发 → 物理清空历史数据 → 拉取全量报价数据 → MQ异步发送消息 → 消费者解析区域上下级 → 批量保存关联数据 → 提供前端查询接口

四、核心代码实现详解

模块1:定时任务(数据刷新入口)

采用XXL-Job实现定时全量刷新,支持手动触发,保证数据最新:

less 复制代码
@Component
@Slf4j
public class OmsDistrictTask {
    @Resource
    private IOmsDistrictService omsDistrictService;

    @XxlJob("OmsDistrictTask")
    public void execute() {
        log.info("开始刷新下单行政区域数据定时任务");
        omsDistrictService.refresh();
        log.info("刷新下单行政区域数据定时任务执行结束");
    }
}

模块2:数据刷新服务(清空+发送MQ)

全量删除历史数据,拉取报价数据,通过MQ异步处理(避免大批量数据阻塞):

ini 复制代码
@Override
public void refresh() {
    log.info("开始刷新下单行政区域数据");
    // 物理删除历史有效数据
    LambdaQueryWrapper<OmsDistrict> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(OmsDistrict::getIsDelete, 0);
    omsDistrictMapper.deleteAbsolute(wrapper);

    // 拉取全量报价线路数据
    List<CostPriceVo> costPriceVos = iCostRest.listZtc();
    log.info("查询报价数据条数:{}", costPriceVos.size());

    // MQ异步发送,分条处理,提升性能
    for (CostPriceVo vo : costPriceVos) {
        String key = vo.getSrcCode() + "-" + vo.getDestCode();
        omsDistrictProvider.send(MqMsgUitl.build(vo, key));
    }
}

模块3:MQ消费者(异步处理核心逻辑)

事务保证数据一致性,批量解析并保存区域关联数据:

less 复制代码
@Slf4j
@Component
@MqConsumer(topic = "oms_district_provider", groupName = "oms_district_consumer")
public class OmsDistrictConsumer extends MqMessageListener<MqMsgVo<CostPriceVo>> {
    @Autowired
    private IOmsDistrictService iOmsDistrictService;

    @Transactional
    @Override
    public void onMsg(MessageExt message, MqMsgVo<CostPriceVo> msgVo) {
        CostPriceVo body = msgVo.getBody();
        if (body == null) {
            log.error("消息体为空,消息ID:{}", message.getMsgId());
            return;
        }
        // 核心:解析并保存区域关联数据
        iOmsDistrictService.saveDistrictBatch(body);
    }
}

模块4:核心业务实现(区域上下级解析)

最核心逻辑 :递归解析始发/目的区域的上级(省/市)+ 下级(区/街道) ,生成全量关联数据:

  1. 解析当前区域的上级(最多3级,直到国家编码);
  2. 查询当前区域的所有子级区域(两级);
  3. 区分始发(type=0)目的(type=1)数据,批量保存。
ini 复制代码
/**
 * 批量保存区域关联数据(核心方法)
 */
public void saveDistrictBatch(CostPriceVo costPriceVo) {
    String srcCode = costPriceVo.getSrcCode();
    String destCode = costPriceVo.getDestCode();
    // 查询行政区域基础信息
    List<DistrictVo> districtList = idistrictRest.listByCodes(ImmutableList.of(srcCode, destCode));
    Map<String, DistrictVo> districtMap = districtList.stream()
            .collect(Collectors.toMap(DistrictVo::getCode, Function.identity()));
    
    DistrictVo srcDistrict = districtMap.get(srcCode);
    DistrictVo destDistrict = districtMap.get(destCode);
    if (srcDistrict == null || destDistrict == null) return;

    // 1. 生成【始发区域】关联数据(type=0)
    List<OmsDistrict> srcSaveList = new ArrayList<>();
    extracted(srcDistrict, destCode, srcSaveList, "0");

    // 2. 生成【目的区域】关联数据(type=1)
    List<OmsDistrict> destSaveList = new ArrayList<>();
    extracted(destDistrict, srcCode, destSaveList, "1");

    // 批量保存,提升性能
    this.saveBatch(srcSaveList);
    this.saveBatch(destSaveList);
}

/**
 * 递归解析:上级区域 + 子级区域
 */
private void extracted(DistrictVo srcVo, String relationCode, List<OmsDistrict> saveList, String type) {
    String parentCode = srcVo.getParentCode();
    // 1. 解析上级区域(省/市,最多3级)
    for (int i = 0; i < 3; i++) {
        if (i == 0) {
            setSrcOrg(srcVo, relationCode, saveList, type);
        } else {
            if ("100000".equals(parentCode)) break; // 国家编码,终止
            DistrictVo parentVo = idistrictRest.queryByCode(Long.parseLong(parentCode));
            if (parentVo == null) break;
            setSrcOrg(parentVo, relationCode, saveList, type);
            parentCode = parentVo.getParentCode();
        }
    }

    // 2. 查询并解析子级区域(区/街道)
    List<DistrictVo> childList = getChildDisrict(srcVo.getCode(), (Integer.parseInt(srcVo.getLevelType()) + 1) + "");
    for (DistrictVo childVo : childList) {
        setSrcOrg(childVo, relationCode, saveList, type);
        // 二级子区域
        List<DistrictVo> secondChildList = getChildDisrict(childVo.getCode(), (Integer.parseInt(childVo.getLevelType()) + 1) + "");
        if (CollectionUtils.isNotEmpty(secondChildList)) {
            secondChildList.forEach(secondVo -> setSrcOrg(secondVo, relationCode, saveList, type));
        }
    }
}

/**
 * 封装区域数据实体
 */
private void setSrcOrg(DistrictVo srcVo, String relationCode, List<OmsDistrict> saveList, String type) {
    OmsDistrict district = new OmsDistrict();
    // 复制行政区域基础信息
    BeanUtil.copyProperties(srcVo, district);
    // 绑定关联的对端编码
    if ("0".equals(type)) district.setDestCode(relationCode);
    if ("1".equals(type)) district.setSrcCode(relationCode);
    district.setType(type);
    saveList.add(district);
}

模块5:数据查询接口(前端交互)

提供三级联动分页查询 + 地址模糊查询 ,SQL通过GROUP BY去重,保证数据唯一:

less 复制代码
@RestController
@RequestMapping("/oms/district")
@Tag(name = "下单行政区域")
public class OmsDistrictController {
    @Autowired
    private IOmsDistrictService omsdistrictService;

    // 手动刷新数据
    @PostMapping("/refresh")
    public WebResponse refresh() {
        omsdistrictService.refresh();
        return WebResponseUtil.success.build();
    }

    // 三级联动分页查询(核心:根据parentCode/type查询)
    @PostMapping("/queryDistrictByPage")
    public PageableEntity<OmsDistrict> queryDistrictByPage(@RequestBody PageRequest<OmsDistrictParam> request) {
        IPage<OmsDistrict> pager = MpToolkit.toPage(request);
        OmsDistrictParam param = request.getParam();
        if (StringUtils.isBlank(param.getParentCode())) param.setParentCode("100000");
        IPage<OmsDistrict> result = omsdistrictService.selectPageByCustomSql(pager, param);
        return MpToolkit.build(result);
    }

    // 地址模糊查询(支持搜索全名称)
    @PostMapping("/associate")
    public PageableEntity<OmsDistrict> associate(@RequestBody PageRequest<OmsDistrictParam> request) {
        IPage<OmsDistrict> pager = MpToolkit.toPage(request);
        return TenantHelper.ignore(() -> omsdistrictService.associate(pager, request.getParam()));
    }
}

模块6:MyBatis 查询SQL

通过GROUP BY去重,精准匹配关联区域:

xml 复制代码
<!-- 三级联动查询 -->
<select id="selectPageByCustomSql" resultType="OmsDistrict">
    SELECT
        code, name,
        MAX(dest_code) AS dest_code,
        MAX(src_code) AS src_code,
        MAX(merger_name) AS merger_name,
        MAX(parent_code) AS parent_code
    FROM oms_district
    WHERE type = #{dto.type} AND parent_code = #{dto.parentCode}
    <if test="dto.destCodeList!= null">AND dest_code IN <foreach collection="dto.destCodeList" open="(" separator="," close=")">#{item}</foreach></if>
    <if test="dto.srcCodeList!= null">AND src_code IN <foreach collection="dto.srcCodeList" open="(" separator="," close=")">#{item}</foreach></if>
    GROUP BY code, name
</select>

<!-- 模糊查询 -->
<select id="associate" resultType="OmsDistrict">
    SELECT merger_name, MAX(merger_code) AS merger_code, MAX(code) AS code, MAX(name) AS name
    FROM oms_district
    WHERE merger_name LIKE CONCAT('%', #{dto.key}, '%')
      AND type = #{dto.type} AND level_type = '4'
    GROUP BY merger_name
    ORDER BY level_type ASC
</select>

五、方案亮点

  1. 性能优化:预生成关联数据,前端查询无实时计算,响应极快;
  2. 异步解耦:MQ处理大批量数据,避免定时任务阻塞;
  3. 树形结构全覆盖:自动解析区域上下级,支持省市区三级联动;
  4. 数据精准:严格基于报价表生成,无冗余/错误区域;
  5. 易维护:定时自动刷新+手动触发,保证数据实时性。

六、适用场景

本方案适用于物流、快递、同城配送等需要严格限制寄/收件区域范围的下单系统,完美解决「地址关联选择」的核心业务痛点,同时兼顾性能与扩展性。

相关推荐
iPadiPhone2 小时前
性能优化的“双刃剑”:MySQL 查询缓存深度架构解析与面试复盘
java·后端·mysql·缓存·面试·性能优化
WmKong2 小时前
告别 GORM 的“魔法字符串”和“事务满天飞”:我开源了一个强类型查询构建库
后端
Meta392 小时前
SpringBoot通过kt-connect+kubectl进行本地调试k8s服务
spring boot·后端·kubernetes
杰杰7982 小时前
深入理解 Django REST Framework 的 Serializer(上)
后端·python·django
tant1an2 小时前
Spring Boot 进阶之路:热部署机制 + 配置高级特性详解
java·spring boot·后端
xiaoye37082 小时前
如何在Spring中使用注解解决线程并发问题?
java·后端·spring
future02102 小时前
Spring IOC启动全流程解密
java·后端·spring·ioc
太阳神LoveU2 小时前
Spring Boot 4.0.3和3.X的各个版本主要功能差别和优劣势对比
java·spring boot·后端
zhoupenghui1682 小时前
golang 锁实现原理与解析&锁机制(sync)种类与举例说明以及其使用场景
开发语言·后端·golang·mutex·wait·lock·sync