物流下单系统寄/到件省市区关联选择功能实现方案
一、需求背景与业务目标
在物流下单场景中,寄件地址与收件地址并非全域可选,需严格匹配后台维护的报价线路规则:
- 基础数据:行政区域表(省/市/区/街道四级树形结构)、报价维护表(维护
始发市/区县 → 目的市/区县的可配送线路); - 核心需求:用户选择寄件行政区域后,收件区域仅展示该寄件区域关联的可配送范围;反之选择收件区域后,寄件区域仅展示关联范围;
- 功能要求:支持省市区三级联动选择、地址模糊查询,保证查询性能与数据准确性。
二、整体技术方案设计
1. 设计思路
为避免实时查询行政区域+报价表导致的性能问题,采用**「预生成关联数据 + 定时全量刷新 + MQ异步处理」**方案:
- 全量拉取报价表的
始发编码(srcCode)、目的编码(destCode); - 递归解析每个编码的上级区域(省/市)+ 下级区域(区/街道) ,构建完整的区域关联关系;
- 将关联数据预存储到专用表
oms_district,前端查询直接命中预生成数据; - 通过定时任务+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:核心业务实现(区域上下级解析)
最核心逻辑 :递归解析始发/目的区域的上级(省/市)+ 下级(区/街道) ,生成全量关联数据:
- 解析当前区域的上级(最多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>
五、方案亮点
- 性能优化:预生成关联数据,前端查询无实时计算,响应极快;
- 异步解耦:MQ处理大批量数据,避免定时任务阻塞;
- 树形结构全覆盖:自动解析区域上下级,支持省市区三级联动;
- 数据精准:严格基于报价表生成,无冗余/错误区域;
- 易维护:定时自动刷新+手动触发,保证数据实时性。
六、适用场景
本方案适用于物流、快递、同城配送等需要严格限制寄/收件区域范围的下单系统,完美解决「地址关联选择」的核心业务痛点,同时兼顾性能与扩展性。