Java TMS运输管理系统源码:运单状态机与调度并发控制实践
简介:TMS运输管理系统整包面向Java开发者、物流信息化学习者及运输企业技术人员,聚焦运输业务后台的订单处理、路线规划与优化、车辆司机调度、成本控制、货物GPS追踪、报表分析以及与ERP、WMS等系统的接口集成,可用于理解运输管理系统的模块划分与业务闭环,也适合课程设计、毕业设计、企业原型验证或二次开发参考。压缩包为zip格式,约74.39MB,上游未提供文件总数及主要文件类型明细,实际内容以包内为准,不宜对目录结构作过度推测。已有2375人浏览学习,说明该主题具有一定关注度。读者可从中梳理Java后台管理系统如何组织运输订单全流程、动态调度资源、沉淀成本与绩效报表,并思考开放接口与可扩展设计,为搭建或改造同类物流系统提供参照。
1. 调度室白板与一张运单的距离
调度室白板上写满车牌号,运单走到哪一步靠电话挨个问,月底对账三个人翻两周的 Excel——这是不少中小运输团队的真实状态。这套 TMS 运输管理系统.zip 就是冲着这个场景来的:一套基于 Java 的运输管理系统源码,标准的 Java 后台管理系统分层结构,把订单、车辆、司机、调度、在途、回单、对账串成一条完整链路。它不是那种只有登录页和菜单的空壳管理系统,运单状态机、调度并发控制、回单附件、对账汇总这些真正费脑子的地方都有对应实现。适合三类人:要在一两周内交付一套内部运输管理系统的开发者、想拿一个业务闭环完整的 Java 后台管理系统练手的熟手、以及手里已经有客户但缺一套可改可扩的运输管理底座的团队。下面按「先看懂骨架、再跑起来、再改业务、最后避坑」的顺序拆。
2. 系统骨架与选型:六张核心表撑起一条运单链路
很多人拿到一套 Java 后台管理系统源码,第一反应是找登录页、把首页跑出来,看到菜单全亮就觉得摸透了。运输管理系统恰恰不能这么看,它的技术难度不在框架,而在业务状态的流转——一张运单从创建到结算要跨七八个节点,每个节点对应不同的表和不同的权限。骨架看不懂,后面改一个字段就可能把对账算错。
2.1 从运单生命周期反推模块划分
先别急着读代码,先看数据怎么流动。任何 TMS 的主线都是一张运单:客户下单生成运单,调度给运单绑定车辆和司机,司机到场提货,发车进入在途,收货方签收,回单寄回或拍照上传,财务按客户汇总出账,最后结算付款。这条线走完,运单才算关闭。
按这条线反推,模块边界就清楚了:
| 环节 | 业务动作 | 模块 | 核心表 |
|---|---|---|---|
| 下单 | 录入货物、收货人、期望时间 | 订单管理 | tms_waybill、tms_waybill_item |
| 调度 | 绑定车辆、司机、计划发车 | 调度管理 | tms_waybill、tms_vehicle、tms_driver |
| 提货发车 | 确认提货、发车、记录里程 | 在途管理 | tms_waybill_track |
| 签收 | 收货方签收、异常登记 | 在途管理 | tms_waybill_track |
| 回单 | 上传回单照片或 PDF | 回单管理 | tms_waybill_receipt |
| 对账 | 按客户、按周期汇总金额 | 财务管理 | tms_waybill_fee |
| 结算 | 账单确认、付款登记 | 财务管理 | tms_settlement |
这里有个反直觉的点:不要按「车辆模块、司机模块、财务模块」这种组织架构切分代码。车辆和司机在系统里本质是运单上的两个外键,它们的独立 CRUD 只是基础数据维护,真正的业务逻辑在运单那一侧。把 service 按「运单生命周期」切,比按「部门职责」切,后期加状态、加节点会省很多事。我见过一个项目把调度逻辑写在 vehicleService 里,结果加一个「改派」需求要改三个 service,血泪经验。
2.2 工程目录与分层
这套源码走的是多模块 Maven 结构,目录大概是这个形状:
tms/ ├── tms-admin/ # 启动类 + Controller,对外接口层 │ └── src/main/java/.../controller/ ├── tms-common/ # 统一返回体、异常、常量、工具类 ├── tms-domain/ # Entity、DTO、VO、枚举 ├── tms-service/ # 业务逻辑,运单状态机在这里 ├── tms-mapper/ # MyBatis Mapper 接口与 XML └── pom.xml # 父 POM,统一版本管理分层本身不新鲜,关键是边界要守住:Controller 只做参数校验、DTO 转换、调用 service,不写 if-else 业务判断;Service 层不允许直接拼 SQL,复杂查询放 Mapper XML;Domain 层不要引入 Spring 注解,保持可单测。很多人图省事把状态流转判断写在 Controller 里,短期快,等到要加一个「批量调度」接口时,逻辑得复制一遍,两边慢慢就不一致了。
技术选型上,这类后台管理系统主流组合是 Spring Boot + MyBatis(或 MyBatis-Plus)+ MySQL + Redis,权限用 Spring Security 或 Shiro,前端要么 Vue 分离部署,要么 Thymeleaf 服务端渲染。为什么不用 JPA?运输管理系统里对账、报表、按客户按线路的多条件汇总查询非常多,JPA 的 Criteria 写起来绕,最后还是得回落原生 SQL,不如一开始就用 MyBatis 把 SQL 攥在手里。Redis 在这里主要干两件事:登录态/Token 缓存,以及运单号序列生成。
提示:如果只是想先跑通看效果,不要动分层结构。多模块拆分的价值在于后期替换某一块(比如把 Redis 换成数据库号段)时影响面可控。
2.3 核心表结构:运单主表怎么设计
运单主表是整个系统的地基,字段设计错一个,后面全是补丁。下面这段建表语句是这类系统里比较典型的一版:
CREATE TABLE tms_waybill ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', waybill_no VARCHAR(32) NOT NULL COMMENT '运单号,业务唯一', customer_id BIGINT NOT NULL COMMENT '客户ID', status TINYINT NOT NULL DEFAULT 10 COMMENT '状态:10待调度 20已调度 30已提货 40在途 50已送达 60已回单 70已对账 80已结算 90已取消', vehicle_id BIGINT DEFAULT NULL COMMENT '车辆ID,调度后写入', driver_id BIGINT DEFAULT NULL COMMENT '司机ID,调度后写入', consignee_name VARCHAR(64) NOT NULL COMMENT '收货人', consignee_phone VARCHAR(20) NOT NULL COMMENT '收货电话', origin_code VARCHAR(16) NOT NULL COMMENT '始发地编码', dest_code VARCHAR(16) NOT NULL COMMENT '目的地编码', goods_name VARCHAR(128) NOT NULL COMMENT '货物名称', weight DECIMAL(12,3) NOT NULL DEFAULT 0 COMMENT '重量(kg)', volume DECIMAL(12,3) NOT NULL DEFAULT 0 COMMENT '体积(m3)', freight_amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '运费', insurance_amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '保价费', plan_pick_time DATETIME DEFAULT NULL COMMENT '计划提货时间', sign_time DATETIME DEFAULT NULL COMMENT '签收时间,对账按此字段取数', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1删除', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_waybill_no (waybill_no), KEY idx_customer_status (customer_id, status), KEY idx_sign_time (sign_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运单主表';几个字段值得单独说。waybill_no必须建唯一索引,运单号是业务主键,重复了就是事故;生成策略常见做法是「前缀 + yyyyMMdd + 6 位日序列」,序列用 Redis 的 INCR 按天做 key,日切时自然重置。status用 TINYINT 不用 VARCHAR,好处是索引小、比较快,坏处是可读性差,所以必须配一个枚举类,代码里禁止裸写数字。金额字段一律DECIMAL(12,2),不要用 DOUBLE,这个坑第 5 章会专门讲。sign_time单独建索引,因为对账是按签收时间取数的,这个索引不加,跑月账时你会盯着慢日志发呆。
其余四张关联表各司其职:tms_waybill_item存多货物明细,一单多品时用;tms_waybill_track存轨迹节点,每发生一次状态变更插一条,带操作人、时间、备注,是排查问题的黑匣子;tms_waybill_receipt存回单附件,只存文件路径不存文件本身;tms_waybill_fee存费用明细,运费、保价费、装卸费、其他费用拆行存,方便后面按费用类型出报表。主表里只放汇总后的金额,明细在 fee 表,这样既能快速列表展示,又能追溯每一笔钱的来源。
3. 环境搭建与启动:从 JDK 到第一个登录页
骨架看完,下一步是让它跑起来。这一步翻车最多,九成问题出在版本对不上和配置没改。下面这套流程是我一般会走的顺序:先对齐环境,再改配置,最后导数据验证。
3.1 依赖清单与环境核对
先把环境清单摆出来,逐项核对,别跳:
| 组件 | 建议版本 | 用途 | 版本不对的典型症状 |
|---|---|---|---|
| JDK | 17(或按 pom 里声明的版本) | 编译运行 | Unsupported class file major version |
| Maven | 3.8 以上 | 构建 | 依赖拉不下来、插件报错 |
| MySQL | 8.0 | 主库 | 连接报 Public Key Retrieval 错误 |
| Redis | 6.0 以上 | 缓存与运单号序列 | 启动时连接超时 |
| Node.js | 18(前端分离时用) | 前端构建 | npm install 卡住或依赖冲突 |
JDK 版本最容易踩雷。老项目用 JDK 8 写的,你拿 JDK 21 去编译,Lombok 先给你脸色看;新项目用 JDK 17 的语法,你拿 JDK 8 跑,编译直接失败。判断方法很简单,打开根目录 pom.xml 看<java.version>或<maven.compiler.source>,以它为准,别凭感觉装。MySQL 8 和 5.7 的驱动类名也不一样,8 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver,配错就是启动即报错。
3.2 配置文件的三个必改项
配置文件通常叫application-dev.yml或类似名字,里面有几十项,但真正必须改的只有三处:数据源、Redis、上传目录。
server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/tms?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false username: tms password: change_me driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 tms: upload: path: /data/tms/upload # 回单附件根目录,不要放在项目目录下serverTimezone这一项很多人省略,结果签收时间写进库差 8 小时,对账时按日期取数直接漏单,这是玄学问题的头号来源,后面第 5 章会展开。allowPublicKeyRetrieval=true是 MySQL 8 的专属坑,不加这个参数,某些认证方式下连接会直接失败。upload.path单独拎出来说,是因为回单附件是业务数据,放在项目目录里意味着每次重新部署打包都可能把它覆盖掉,血泪教训,一定要指到项目之外的独立目录,并且这个目录要纳入备份。
注意:生产环境的数据库密码、Redis 密码不要写死在 yml 里提交到代码库,用环境变量或配置中心覆盖,这套源码默认的 dev 配置只是给本地跑通用的。
3.3 初始化 SQL 与启动验证
配置改完,按顺序执行下面四步:
# 1. 建库,字符集必须是 utf8mb4,否则收货人姓名里的生僻字会变问号 mysql -u root -p -e "CREATE DATABASE tms DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" # 2. 导表结构,再导基础数据(菜单、角色、字典) mysql -u root -p tms < sql/schema.sql mysql -u root -p tms < sql/data.sql # 3. 编译打包,第一次先跳过测试,把环境问题和技术问题分开 mvn clean package -DskipTests # 4. 以 dev 配置启动 java -jar tms-admin/target/tms-admin.jar --spring.profiles.active=dev导入顺序不能反,data.sql 里的菜单和角色关联依赖 schema.sql 建好的表。启动成功后不要只看「Started Application」,按这三条验证才算通:第一,浏览器打开登录页,用初始化脚本里的默认账号能登进去;第二,左侧菜单能完整渲染,说明角色权限数据导对了;第三,随便点开一个列表页,看接口返回的 code 是不是 200、分页 total 是不是大于 0,这能确认数据库连接和 Mapper 映射都没问题。三条都过,环境这关就算过了,可以开始改业务。
4. 运单主流程落地:下单→调度→在途→回单→对账
环境跑通只是起点,真正要改的是业务。这一章按运单的生命周期走一遍,每一站给一段能直接抄的代码或 SQL,重点讲清楚参数为什么这么设、失败时看哪里。
4.1 状态机:把「运单在哪」变成一个字段
运输管理系统最怕的状态是「这张单到底走到哪了」,口头问、微信问、翻表格,都是因为状态没有单一可信来源。解法是把状态收敛到一个字段,并且用一张流转表把合法跳转钉死:
| 当前状态 | 含义 | 触发动作 | 允许流转到 |
|---|---|---|---|
| 10 | 待调度 | 下单成功 | 20、90 |
| 20 | 已调度 | 绑定车辆司机 | 30、10、90 |
| 30 | 已提货 | 司机确认提货 | 40 |
| 40 | 在途 | 发车 | 50 |
| 50 | 已送达 | 收货方签收 | 60 |
| 60 | 已回单 | 回单审核通过 | 70 |
| 70 | 已对账 | 账单确认 | 80 |
| 80 | 已结算 | 付款登记 | — |
| 90 | 已取消 | 取消运单 | — |
这张表要落到代码里,做成枚举或配置,每次状态变更前先校验当前状态 → 目标状态是否在允许集合内,不在就抛业务异常。好处是:任何非法跳转(比如已签收的单被改回待调度)在入口就被拦住,而不是等到对账时发现金额对不上再回头查。状态变更的同时往tms_waybill_track插一条轨迹,记下操作人、时间、变更前后状态、备注,这张轨迹表就是事后排查的黑匣子。
4.2 下单接口:校验、幂等与运单号生成
下单是整条链路的入口,这里出问题,后面全乱。核心是三件事:必填校验、幂等控制、运单号生成。
@PostMapping("/waybill") public Result<Long> create(@RequestBody @Valid WaybillCreateDTO dto) { // 幂等键:客户ID + 客户侧外部单号,防止前端重复提交或网络重试造成重复下单 String idempotentKey = "tms:wb:create:" + dto.getCustomerId() + ":" + dto.getExternalNo(); Boolean first = redisTemplate.opsForValue() .setIfAbsent(idempotentKey, "1", Duration.ofMinutes(10)); if (Boolean.FALSE.equals(first)) { throw new BizException("该外部单号已提交,请勿重复下单"); } // 运单号:TMS + 日期 + 6位日序列,Redis 按天自增 String datePart = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); Long seq = redisTemplate.opsForValue().increment("tms:wb:seq:" + datePart); String waybillNo = "TMS" + datePart + String.format("%06d", seq); Waybill waybill = new Waybill(); waybill.setWaybillNo(waybillNo); waybill.setStatus(WaybillStatus.WAIT_DISPATCH.getCode()); // ... DTO 到 Entity 的其他字段映射 waybillMapper.insert(waybill); return Result.ok(waybill.getId()); }setIfAbsent是 Redis 的 SETNX 语义,10 分钟过期,覆盖了「用户手抖点两次」和「前端超时重试」两种场景。运单号的日期段用BASIC_ISO_DATE拿到20240501这种格式,序列按天做 key,第二天自然从 1 开始,不会无限增长。这里有个细节要注意:Redis 的 INCR 是原子操作,但如果你所在环境对 Redis 持久化没把握,重启丢 key 会导致序列回退、运单号重复,所以唯一索引是最后一道防线,插入失败要捕获并重试一次。
4.3 调度与接单:两个调度员同时点怎么办
调度是并发冲突的高发区。两个调度员同时打开同一张待调度运单,一个先绑了车,另一个后提交,如果不做控制,后提交的会覆盖前一个,车辆司机全乱。解法是乐观锁,运单表上的version字段就是干这个的。
<update id="dispatch"> UPDATE tms_waybill SET status = 20, vehicle_id = #{vehicleId}, driver_id = #{driverId}, version = version + 1 WHERE id = #{waybillId} AND status = 10 AND version = #{version} AND deleted = 0 </update>@Transactional(rollbackFor = Exception.class) public void dispatch(Long waybillId, Long vehicleId, Long driverId, Integer version) { // 车辆、司机必须存在且可用,先校验再更新,避免脏数据 checkVehicleAvailable(vehicleId); checkDriverAvailable(driverId); int rows = waybillMapper.dispatch(waybillId, vehicleId, driverId, version); if (rows == 0) { throw new BizException("运单已被他人调度,请刷新后重试"); } trackService.record(waybillId, WaybillStatus.DISPATCHED, "调度完成"); }SQL 里status = 10 AND version = #{version}是关键,两个条件任何一个不满足,影响行数就是 0,Java 侧据此抛异常。前端拿到这个异常要提示「刷新后重试」,不要静默失败。有人会问为什么不直接SELECT ... FOR UPDATE加行锁,也能解决,但锁的粒度大、持有时间长,调度这种低频但要求响应快的操作,乐观锁更合适。checkVehicleAvailable这类校验要放在更新之前,因为乐观锁只能保证状态和版本一致,保证不了车辆本身是不是已经报废。
4.4 回单与对账:金额从哪来、按什么取数
回单环节的落点是附件上传,文件存磁盘、路径存tms_waybill_receipt,同时把运单状态从 50 推到 60。这里唯一的注意事项是文件大小限制要和 yml 里的max-file-size对齐,不然用户传一张 8MB 的回单照片,前端报 413,后端日志干干净净,排查起来很费时间。
对账才是财务最关心的。核心是一条按客户、按签收时间区间的汇总 SQL:
SELECT w.customer_id, COUNT(*) AS waybill_cnt, SUM(w.freight_amount) AS freight_total, SUM(w.insurance_amount) AS insurance_total, SUM(w.freight_amount + w.insurance_amount) AS payable_total FROM tms_waybill w WHERE w.status = 60 AND w.sign_time >= #{startTime} AND w.sign_time < #{endTime} AND w.deleted = 0 GROUP BY w.customer_id;三个参数要盯死:status = 60表示只对已回单的运单出账,未回单的不参与;时间区间用左闭右开>= start AND < end,避免月末 23:59:59 这种边界重复或漏算;deleted = 0过滤逻辑删除。这条 SQL 跑得慢的话,先看idx_sign_time和idx_customer_status两个索引有没有生效,再看是不是把status写成了字符串导致索引失效——这是很常见的翻车点。
5. 避坑与排查:五个上线后才暴露的问题
本地跑通和线上稳定是两回事。下面这五个问题,都是那种「测试环境一切正常、上线第一周集中爆发」的类型,按现象、原因、解决三段写,遇到时可以直接对号入座。
5.1 时间字段:跨时区取数全错一天
现象:财务按签收日期跑月账,发现有些单子明明 5 月 31 日签收,却出现在 6 月账单里,或者干脆消失。
原因:JDBC 连接串没写serverTimezone,或者写了个和数据库实际时区不一致的值,导致 Java 的LocalDateTime写进 MySQL 时被驱动偷偷偏移了 8 小时。签收时间一旦偏到第二天零点以后,按日期区间取数就错位。
解决:连接串统一加serverTimezone=GMT%2B8(按实际时区调整),并且约定全系统时间字段统一用DATETIME而不是TIMESTAMP,因为TIMESTAMP会随时区转换,DATETIME存什么读什么。改完之后,把历史数据里签收时间异常的运单捞出来,按操作日志重算一遍回填。
5.2 金额用 double:对账差三分钱
现象:单条运单金额看着都对,一汇总就比手工算的少几分钱,客户财务追着问。
原因:建表时图省事用了DOUBLE存运费,浮点数在二进制里表示不精确,0.1 + 0.2 不等于 0.3,几千条累加下来误差就被放大。
解决:所有金额字段改DECIMAL(12,2),Java 侧对应BigDecimal,并且统一用setScale(2, RoundingMode.HALF_UP)做舍入。改字段类型时注意,MySQL 从 DOUBLE 转 DECIMAL 会做一次隐式舍入,转完要抽样核对,别直接在生产上跑 DDL 就完事。
5.3 状态并发:同一张单被调度两次
现象:车辆 A 和车辆 B 的调度记录里出现同一张运单,司机到场发现货已经被别人拉走。
原因:调度接口没做并发控制,两个请求几乎同时读到status = 10,然后各自执行更新,后写的覆盖先写的。
解决:按 4.3 节的乐观锁方案,在 UPDATE 的 WHERE 里带上status和version,影响行数为 0 就抛异常。已经产生脏数据的,靠tms_waybill_track里的操作记录还原时间线,人工确认哪次调度有效。
5.4 分页 count 与多表 join
现象:列表页显示总共 200 条,翻到第 3 页就空了;或者 total 明显偏大,翻到后面全是重复行。
原因:分页插件自动生成的 count 语句在多表 join 时没去重,一对多关联(一张运单对应多条货物明细)把行数放大了。
解决:给 count 查询单独写 SQL,用COUNT(DISTINCT w.id)去重;或者在 Mapper 里显式指定 count 语句,不依赖插件自动生成。这个问题在运单列表关联货物明细时特别容易出现,写完分页一定要拿真实数据翻到最后一页验证。
5.5 日志与慢 SQL:出问题时没有后悔药
现象:线上反馈某张单状态不对,翻日志发现只打了「操作成功」,没有任何入参和状态变更记录。
原因:日志级别配得太粗,业务关键路径没打点,SQL 也没开慢查询日志,出了问题只能靠猜。
解决:三件事一起做——业务关键操作(下单、调度、状态变更、对账)统一打 INFO 日志,带上运单号和变更前后状态;MyBatis 的 SQL 日志在 dev 开、prod 关,但慢查询日志在 prod 必须开,阈值设 1 秒;tms_waybill_track这类轨迹表本身就是业务层面的日志,别省这张表。等真出事再去补日志,那几天只能干着急。
6. 进阶核账:把运费规则抽成可配置表达式
运费规则是运输管理系统里改得最频繁的东西。今天按重量计价,明天客户要求重量加体积取大,后天又要加一条「满 500 公斤每公斤降两毛」。如果这些规则硬编码在 service 里,每改一次就要发一次版,测试回归一遍,成本高得离谱。
比较稳妥的做法是把规则外置成一张表,表达式用 Aviator 或 SpEL 求值:
// 规则表字段:id, customer_id, rule_name, expression, priority, enabled // expression 示例:"#weight * #pricePerKg + #baseFee" public BigDecimal calcFreight(Long customerId, Waybill waybill) { FreightRule rule = ruleMapper.selectActiveByCustomer(customerId); if (rule == null) { throw new BizException("客户未配置运费规则"); } Map<String, Object> env = new HashMap<>(); env.put("weight", waybill.getWeight()); env.put("volume", waybill.getVolume()); env.put("pricePerKg", rule.getPricePerKg()); env.put("baseFee", rule.getBaseFee()); Expression exp = AviatorEvaluator.compile(rule.getExpression(), true); // true 表示缓存编译结果 Object result = exp.execute(env); return new BigDecimal(result.toString()).setScale(2, RoundingMode.HALF_UP); }参数说明:compile(expr, true)的第二个参数开启编译缓存,同一表达式不会重复解析,规则多的时候性能差别明显;env里放的是规则可引用的变量,新增变量要同步更新规则配置页的说明,否则配错一个变量名,运行时报的是看不出所以然的空指针。规则表加priority字段是为了支持多规则命中时的优先级,加enabled是为了灰度启停。
规则改完不能直接上线,必须回放验证。我一般会把上个月已结算的运单导出来,用新规则重算一遍运费,和已结算金额逐单比对,差异超过阈值的挑出来人工看。这个回放脚本不复杂,本质就是循环调calcFreight,但它能挡住九成的规则配置错误。从那以后我每次改运费规则,都强制走一遍「导出历史单 → 重算 → 比对差异 → 抽检确认」这四步,再急也不跳。
希望帮到你。
本文还有配套的精品资源,点击获取