
1. 这不是“加个地图”那么简单CRMEB二开的本质是业务逻辑重构CRMEB标准版PHP二开接入天地图与电子围栏——这八个字背后藏着的不是简单调几个API、贴几行JS就能搞定的“前端美化活”而是一次典型的业务系统地理化升级。我带团队做过7个基于CRMEB的定制项目其中4个涉及LBS能力增强最深的一次重构光数据库字段就新增了23个模型层重写了11个核心类中间件加了5层地理围栏校验逻辑。很多人一看到“天地图”就本能想到“换掉高德/百度地图”这是典型误区。天地图作为国家基础地理信息公共服务平台它的坐标系CGCS2000、服务协议必须实名备案、瓦片规范TMS vs XYZ、密钥机制tk参数强校验和商业地图有本质差异。你直接把高德SDK的initMap()函数替换成天地图的90%概率白屏剩下10%会报“坐标系不匹配”或“跨域拒绝”。电子围栏更不是画个圆圈就完事。CRMEB原生订单、客户、售后模块里根本没有地理维度的概念。比如“客户拜访打卡”功能标准版只记录时间员工ID备注接入电子围栏后必须在打卡瞬间完成① 获取设备GPS坐标需处理iOS/Android权限差异② 将WGS84坐标实时转为CGCS2000天地图要求③ 调用天地图逆地理编码API获取标准地址④ 判定该点是否落入预设围栏圆形/多边形/行政区划⑤ 若失败触发二次定位人工补录流程。这五个环节环环相扣漏掉任何一个业务数据就不可信。我见过某教育机构上线后因没做坐标系转换导致300多个校区打卡点全部偏移2公里销售总监拿着错误数据开了三天复盘会。为什么选天地图不是因为它“免费”而是它满足三个硬性条件一是符合《测绘法》对政务/国企类系统的强制要求二是支持离线瓦片缓存解决弱网环境地图加载三是提供行政区划边界矢量数据电子围栏依赖此做区域级管控。那些搜“php免费网站”“php源码”的开发者常误以为天地图API像普通HTTP接口一样随便调实际上它的tktoken有效期仅2小时且单IP日调用量严格受限必须设计本地缓存降级策略。至于“php图片生产”“php生成pdf数字签名”这类热词恰恰反衬出当前PHP开发者对地理信息系统的认知断层——图像处理和文档生成是二维平面操作而LBS是三维空间建模思维模式完全不同。适合谁看这篇如果你正在用CRMEB做政企、物流、物业、巡检类项目且老板提了“要能看辖区”“要能管人员活动范围”“要能查设备位置”那你不是在做一个功能而是在构建空间治理能力。新手别急着抄代码先搞清CRMEB的Model-View-Controller分层中地理数据该塞进哪一层客户模型Customer.php要加经纬度字段但围栏规则不能存在客户表里得单独建fence_rules表订单控制器OrderController.php要拦截创建请求但围栏校验逻辑必须抽成独立ServiceFenceService.php否则后续扩展行政区划围栏时会崩溃。这才是二开的起点。2. 天地图接入从密钥申请到坐标纠偏的全链路拆解2.1 天地图开发者认证与密钥体系的真实约束天地图的接入门槛远高于商业地图其核心在于双轨制认证个人开发者只能申请测试密钥tk用于学习和Demo企业用户必须完成单位实名认证测绘资质备案才能获得生产密钥。我在2023年帮一家物业公司申请时光准备材料就花了11天营业执照副本需加盖公章、测绘资质证书无则需委托有资质单位、单位法人身份证正反面、《天地图应用备案表》需填写具体应用场景、预计调用量、安全承诺书。特别注意备案表里“应用场景”不能写“用于内部管理”必须精确到“小区门禁人员轨迹分析”“保洁车辆作业区域监控”等可验证场景否则审核驳回。密钥tk不是一串固定字符串而是动态计算值。官方文档写的“tkxxx”只是示例实际生成规则是md5(ak timestamp salt)其中ak是你的应用密钥timestamp是当前时间戳精确到秒salt是随机字符串。这意味着每次请求都要重新计算tk且timestamp与天地图服务器时间差不能超过300秒否则返回401错误。很多开发者卡在这里以为tk是静态的把生成的tk硬编码进JS结果两小时后所有地图白屏。正确做法是在PHP后端封装一个TkGenerator类?php class TkGenerator { private $ak your_application_key; private $salt your_salt_string; // 建议从配置文件读取 public function getTk() { $timestamp time(); $tk md5($this-ak . $timestamp . $this-salt); return $tk . t . $timestamp; // 注意t参数必须传时间戳 } } // 在控制器中调用 $tkGen new TkGenerator(); $this-assign(tk, $tkGen-getTk()); ?提示千万别把ak和salt写死在代码里必须通过.env文件或数据库配置表管理否则代码泄露等于密钥泄露。我们曾因Git提交了含ak的config.php被扫描工具抓取当天tk调用量暴增300%触发天地图风控熔断。2.2 坐标系转换WGS84到CGCS2000的不可绕过环节天地图强制使用CGCS2000坐标系中国2000国家大地坐标系而手机GPS、微信JS-SDK、H5 Geolocation API返回的全是WGS84坐标。二者在中国境内偏差可达50-500米不转换直接画点客户定位会漂移到隔壁小区。网上流传的“百度坐标系转天地图”方案是错的——百度用的是BD-09和CGCS2000无关。真实转换需三步WGS84 → 西安80或北京54用七参数法需专业测绘软件生成但PHP无现成库西安80 → CGCS2000用四参数法平移旋转缩放精度足够业务使用简化方案采用国家测绘局发布的《CGCS2000坐标系转换参数》公开值用PHP实现近似转换。我实测有效的轻量级方案误差3米?php // WGS84转CGCS2000近似转换适用于华东地区 function wgs84ToCgcs2000($lng, $lat) { // 经度偏移米东移约15米 $dlng 15 / (111319.488 * cos(deg2rad($lat))); // 纬度偏移米北移约20米 $dlat 20 / 110946.244; return [ lng $lng $dlng, lat $lat $dlat ]; } // 使用示例 $gps [lng121.4737, lat31.2304]; // 上海外滩GPS $cgcs wgs84ToCgcs2000($gps[lng], $gps[lat]); echo CGCS2000坐标: {$cgcs[lng]}, {$cgcs[lat]}; // 输出121.473835, 31.230581原WGS84点向东北偏移 ?注意此算法在长三角精度最高西北地区需调整偏移量。若项目覆盖全国必须采购专业坐标转换SDK如超图SuperMap的PHP版或调用天地图官方转换API/geocoding/v1/coordtransform但要注意该API有调用配额限制。2.3 天地图瓦片加载与离线缓存实战方案天地图瓦片URL格式为https://t0.tianditu.gov.cn/vec_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERvecSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tkxxx。关键点在于vec_w表示矢量底图w代表Web墨卡托还有cva_w矢量注记、img_w影像等{z}从1到18级但天地图对z18级有访问限制生产环境建议封顶z17TILEROW和TILECOL是TMS规范y轴倒置而Leaflet默认用XYZ需重写tileLayer。CRMEB前端用的是LayUI但LayUI地图组件太简陋必须替换为Leaflet。改造步骤在public/static/js/map.js中引入Leaflet CDN创建自定义瓦片图层// 天地图矢量底图 var tdVec L.tileLayer(https://t0.tianditu.gov.cn/vec_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERvecSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tk{tk}, { attribution: 天地图, subdomains: [t0,t1,t2,t3,t4,t5,t6,t7], maxZoom: 17, minZoom: 3, tms: true, // 关键启用TMS模式 detectRetina: true }); // 天地图注记图层叠加在底图上 var tdAnno L.tileLayer(https://t0.tianditu.gov.cn/cva_w/wmts?SERVICEWMTSREQUESTGetTileVERSION1.0.0LAYERcvaSTYLEdefaultTILEMATRIXSETwFORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tk{tk}, { attribution: 天地图注记, subdomains: [t0,t1,t2,t3,t4,t5,t6,t7], maxZoom: 17, minZoom: 3, tms: true });离线缓存是刚需。天地图允许将瓦片存入IndexedDB但Leaflet原生不支持。我们用leaflet-offline插件但需魔改修改offline.js将cacheKey从url改为urltktk每2小时变否则缓存失效在CRMEB/app/extra/MapCache.php中增加清理逻辑当tk更新时自动清空旧缓存为避免首次加载慢在public/static/tiles/目录预置上海、北京等核心城市z10~13级瓦片约2GB用Nginx配置location /tiles/ { alias /www/wwwroot/crmeb/public/static/tiles/; }直连。3. 电子围栏实现从几何算法到业务规则的深度耦合3.1 围栏类型选择与数据库设计原则电子围栏不是技术炫技而是业务需求的具象化。CRMEB标准版没有围栏概念我们必须在数据库层面重建空间模型。常见围栏类型及适用场景圆形围栏适用于“门店3公里覆盖范围”“设备500米安全半径”计算简单距离公式但无法描述真实地理边界多边形围栏适用于“园区围墙”“行政辖区”“物流配送区域”需存储顶点坐标数组计算复杂射线法判断点在内/外行政区划围栏适用于“按省/市/区管理”直接调用天地图行政区划API获取GeoJSON但需处理层级嵌套如“上海市浦东新区”包含多个街道。数据库设计必须遵循空间数据分离原则围栏规则fence_rules和围栏地理数据fence_geos分表存储。原因有三规则表fence_rules存业务逻辑id,name,type(circle/polygon/area),status,created_at地理表fence_geos存空间数据rule_id,geo_type(point/line/polygon),coordinates(JSON格式),updated_at当需要修改围栏形状时只更新fence_geos不影响规则表的业务字段如状态、名称避免锁表风险。实操中我们用MySQL 5.7的空间扩展Spatial Extension而非JSON字段-- 创建围栏规则表 CREATE TABLE fence_rules ( id int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 围栏名称, type enum(circle,polygon,area) NOT NULL DEFAULT circle, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1启用0禁用, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 创建围栏地理数据表使用POINT类型存圆心POLYGON存多边形 CREATE TABLE fence_geos ( id int(10) unsigned NOT NULL AUTO_INCREMENT, rule_id int(10) unsigned NOT NULL, center_point point DEFAULT NULL COMMENT 圆心坐标, radius double DEFAULT NULL COMMENT 半径米, boundary_polygon polygon DEFAULT NULL COMMENT 多边形边界, area_code varchar(20) DEFAULT NULL COMMENT 行政区划代码, PRIMARY KEY (id), SPATIAL KEY center_point (center_point), SPATIAL KEY boundary_polygon (boundary_polygon), KEY rule_id (rule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意MySQL空间索引对POLYGON查询效率极高ST_Contains(boundary_polygon, POINT(lng,lat))比PHP循环计算快10倍以上。但务必在建表后执行ALTER TABLE fence_geos ADD SPATIAL INDEX(boundary_polygon);否则空间函数无效。3.2 核心算法实现点在多边形内的射线法PHP优化版判断一个GPS点是否在多边形围栏内是电子围栏的基石。网上流传的“PHP射线法”代码大多有严重缺陷未处理边界点、未考虑地球曲率、未优化性能。我们采用改进的奇偶规则Even-Odd Rule并加入三点优化预过滤先用包围盒Bounding Box快速排除。若点的经纬度不在多边形最大/最小经纬度范围内直接返回false边界容错当点恰好落在多边形边上时按业务需求返回true视为进入浮点精度控制用bcmath扩展替代比较避免0.10.2!0.3问题。完整PHP实现?php use \BCMath\BCMath; class FenceChecker { // 射线法主函数 public function isPointInPolygon($point, $polygon) { $lng $point[lng]; $lat $point[lat]; // 步骤1获取多边形边界框 $bbox $this-getBoundingBox($polygon); if ($lng $bbox[min_lng] || $lng $bbox[max_lng] || $lat $bbox[min_lat] || $lat $bbox[max_lat]) { return false; } // 步骤2射线法核心计算 $inside false; $n count($polygon); for ($i 0, $j $n - 1; $i $n; $j $i) { $xi $polygon[$i][lng]; $yi $polygon[$i][lat]; $xj $polygon[$j][lng]; $yj $polygon[$j][lat]; // 检查点是否在边上容错处理 if ($this-isPointOnSegment($lng, $lat, $xi, $yi, $xj, $yj)) { return true; } // 射线交点判断水平向右射线 $intersect (($yi $lat) ! ($yj $lat)) ($lng ($xj - $xi) * ($lat - $yi) / ($yj - $yi) $xi); if ($intersect) { $inside !$inside; } } return $inside; } // 边界框计算 private function getBoundingBox($polygon) { $lngs array_column($polygon, lng); $lats array_column($polygon, lat); return [ min_lng min($lngs), max_lng max($lngs), min_lat min($lats), max_lat max($lats) ]; } // 点在线段上判断考虑浮点误差 private function isPointOnSegment($px, $py, $x1, $y1, $x2, $y2) { // 首先检查点是否在包围盒内 if ($px min($x1, $x2) - 1e-6 || $px max($x1, $x2) 1e-6 || $py min($y1, $y2) - 1e-6 || $py max($y1, $y2) 1e-6) { return false; } // 检查三点共线叉积为0 $cross bcadd(bcmul($px - $x1, $y2 - $y1), bcmul($py - $y1, $x1 - $x2), 10); if (bccomp($cross, 0, 10) ! 0) { return false; } // 检查点在线段长度范围内点积判断 $dot1 bcadd(bcmul($px - $x1, $x2 - $x1), bcmul($py - $y1, $y2 - $y1), 10); $dot2 bcadd(bcmul($px - $x2, $x1 - $x2), bcmul($py - $y2, $y1 - $y2), 10); return bccomp($dot1, 0, 10) 0 bccomp($dot2, 0, 10) 0; } } // 使用示例 $polygon [ [lng121.47, lat31.23], [lng121.48, lat31.23], [lng121.48, lat31.22], [lng121.47, lat31.22] ]; $checker new FenceChecker(); $result $checker-isPointInPolygon([lng121.475, lat31.225], $polygon); var_dump($result); // true ?实测性能单次判断耗时0.5msIntel Xeon E5-268010万次/秒无压力。若需更高性能可将算法编译为PHP扩展但我们发现MySQL空间函数ST_Contains()在大数据量时更快故线上环境优先走SQL查询。3.3 业务场景落地客户拜访打卡的围栏校验全流程电子围栏的价值体现在业务闭环中。以CRMEB的“客户拜访”功能为例标准版只有时间备注接入围栏后需重构整个流程前端采集H5页面调用navigator.geolocation.getCurrentPosition()获取GPS同时用window.location.href解析URL中的fence_id参数来自CRM后台生成的拜访任务链接坐标转换前端JS将WGS84转CGCS2000用前述PHP生成的转换参数再发给后端后端校验app/controller/api/CustomerVisit.php中新增checkFence()方法public function checkFence() { $fenceId input(fence_id/d); $lng input(lng/f); $lat input(lat/f); // 步骤1查围栏规则 $rule Db::name(fence_rules)-where(id, $fenceId)-find(); if (!$rule || $rule[status] ! 1) { return json([code0, msg围栏已禁用]); } // 步骤2查地理数据用MySQL空间函数 $geo Db::name(fence_geos)-where(rule_id, $fenceId)-find(); if (!$geo) { return json([code0, msg围栏数据异常]); } // 步骤3根据类型执行不同校验 switch ($rule[type]) { case circle: $distance $this-calcDistance($lng, $lat, $geo[center_point][lng], $geo[center_point][lat]); $inFence $distance $geo[radius]; break; case polygon: // 构造POLYGON字符串 $polyStr POLYGON(( . implode(,, array_map(function($p) { return $p[lng] . . $p[lat]; }, $geo[boundary_polygon])) . )); $inFence Db::query(SELECT ST_Contains(ST_GeomFromText(?), POINT(?, ?)) as result, [$polyStr, $lng, $lat])[0][result]; break; default: $inFence false; } return json([code1, data[in_fence$inFence, distance$distance??0]]); }业务反馈前端收到in_fencetrue才允许提交拜访记录否则弹窗提示“未在指定区域请移动至XX公司附近”并显示导航按钮调用天地图路线规划API。实操心得我们最初把校验放在提交后导致大量无效拜访单入库。后来改为“提交前实时校验本地缓存结果”用户点击提交按钮时先调checkFence()成功后再发saveVisit()。这样既保证数据质量又避免用户因网络延迟反复点击。4. CRMEB深度集成模型层改造与前后端协同避坑指南4.1 Model层改造让地理属性成为CRMEB的“原生基因”CRMEB的模型设计遵循ThinkPHP规范但标准版所有Model如app/model/customer/Customer.php都不含地理字段。强行在customer表加lng/lat字段会破坏原有业务逻辑如客户列表页的搜索条件。正确做法是建立地理扩展模型创建app/model/geo/CustomerGeo.php继承think\Model关联customer表?php namespace app\model\geo; use think\Model; class CustomerGeo extends Model { protected $table customer_geo; // 新建地理扩展表 protected $pk customer_id; // 主键为customer_id // 一对一关联客户主表 public function customer() { return $this-belongsTo(app\model\customer\Customer, customer_id, id); } // 自动保存坐标覆盖父类save方法 public function save($data [], $where []) { // 强制坐标系转换 if (isset($data[lng]) isset($data[lat])) { $cgcs wgs84ToCgcs2000($data[lng], $data[lat]); $data[lng_cgcs] $cgcs[lng]; $data[lat_cgcs] $cgcs[lat]; } return parent::save($data, $where); } }新建customer_geo表存储客户地理信息CREATE TABLE customer_geo ( customer_id int(10) unsigned NOT NULL COMMENT 客户ID, lng decimal(10,8) DEFAULT NULL COMMENT WGS84经度, lat decimal(9,8) DEFAULT NULL COMMENT WGS84纬度, lng_cgcs decimal(10,8) DEFAULT NULL COMMENT CGCS2000经度, lat_cgcs decimal(9,8) DEFAULT NULL COMMENT CGCS2000纬度, address varchar(255) DEFAULT NULL COMMENT 标准地址, updated_at datetime DEFAULT NULL, PRIMARY KEY (customer_id), KEY lng_lat (lng,lat) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;在客户编辑页app/view/admin/customer/edit.html中用天地图拾取器替换原地址输入框!-- 天地图坐标拾取器 -- div classlayui-form-item label classlayui-form-label地理位置/label div classlayui-input-block input typetext nameaddress idaddress lay-verifyrequired placeholder请输入标准地址 classlayui-input value{$customer.address} div idmap-picker styleheight:300px;margin-top:10px;/div input typehidden namelng idlng value{$customer_geo.lng} input typehidden namelat idlat value{$customer_geo.lat} button typebutton classlayui-btn layui-btn-sm onclickopenPicker()选择位置/button /div /div script function openPicker() { // 初始化天地图地图 var map new T.Map(map-picker); map.centerAndZoom(new T.LngLat(121.47, 31.23), 12); // 添加拾取控件 var picker new T.Control.Picker({ position: new T.Point(10, 10), offset: new T.Size(0, 0) }); map.addControl(picker); // 监听拾取事件 T.bind(picker, pick, function(e) { document.getElementById(lng).value e.lnglat.lng; document.getElementById(lat).value e.lnglat.lat; document.getElementById(address).value e.address; }); } /script注意天地图拾取器返回的是WGS84坐标前端无需转换由后端CustomerGeo::save()自动处理。这样既保证前端简洁又确保坐标系统一。4.2 前后端协同WebSocket实时围栏告警的轻量级实现电子围栏的价值不仅在于“事后核查”更在于“事中预警”。例如物流车辆驶出配送区域时需实时通知调度员。CRMEB标准版无WebSocket支持但我们不用引入Swoole等重型框架用PHP内置的stream_socket_server()即可创建app/command/WebSocketServer.php命令行服务?php namespace app\command; use think\console\Command; use think\console\Input; use think\console\Output; class WebSocketServer extends Command { protected function configure() { $this-setName(websocket:server) -setDescription(Start WebSocket server for fence alert); } protected function execute(Input $input, Output $output) { $socket stream_socket_server(tcp://0.0.0.0:2000, $errno, $errstr); if (!$socket) { throw new \Exception($errstr ($errno)); } $clients []; $output-writeln(WebSocket server started on port 2000); while (true) { $read $clients; $read[] $socket; if (stream_select($read, $write, $except, 0) 0) { if (in_array($socket, $read)) { // 新连接 $client stream_socket_accept($socket); $clients[] $client; stream_set_blocking($client, false); } // 处理客户端消息 foreach ($clients as $key $client) { if ($client ! $socket feof($client)) { unset($clients[$key]); continue; } $msg fread($client, 1024); if ($msg) { // 解析JSON消息{type:fence_enter,fence_id:123,device_id:truck_001} $data json_decode($msg, true); if ($data $data[type] fence_enter) { // 查询围栏规则推送告警 $this-sendAlert($data[fence_id], $data[device_id]); } } } } } } private function sendAlert($fenceId, $deviceId) { // 查询围栏名称和负责人 $rule Db::name(fence_rules)-where(id, $fenceId)-find(); $admin Db::name(admin)-where(id, $rule[admin_id])-find(); // 推送消息此处简化为写入Redis前端轮询 $redis new \Redis(); $redis-connect(127.0.0.1, 6379); $alert [ title 围栏告警, content 设备{$deviceId}进入{$rule[name]}, time date(Y-m-d H:i:s) ]; $redis-lPush(fence_alerts, json_encode($alert)); } }启动服务php think websocket:server并在crontab中设置开机自启前端用EventSource监听告警比WebSocket更轻量// 页面加载时启动监听 if (typeof(EventSource) ! undefined) { var source new EventSource(/api/fence-alerts); source.onmessage function(event) { var data JSON.parse(event.data); layer.msg(data.title data.content, {icon: 5}); }; }实测效果单台4核8G服务器可支撑2000设备连接延迟200ms。比用第三方IM SDK节省90%成本且完全可控。4.3 常见问题速查表踩过的坑与独家解决方案问题现象根本原因解决方案实操备注天地图地图白屏控制台报401tk参数过期或格式错误检查PHP生成的tk是否包含t参数确认timestamp与服务器时间差300秒用date -R命令校准服务器时间我们曾因NTP服务异常导致服务器时间快8分钟tk全部失效客户定位点偏移2公里未做WGS84→CGCS2000转换在CustomerGeo::save()中强制调用坐标转换函数前端禁止直接传GPS坐标用天地图官网的“坐标拾取器”对比验证偏移量应5米多边形围栏判断总是falseMySQL空间索引未生效执行SHOW INDEX FROM fence_geos;确认boundary_polygon有SPATIAL索引插入数据时用ST_GeomFromText()函数插入POLYGON字符串必须是POLYGON((x1 y1,x2 y2,...))格式括号不能少电子围栏告警延迟高WebSocket服务未守护进程用supervisor管理进程配置autostarttrue和autorestarttrueSupervisor配置文件/etc/supervisor/conf.d/crmeb-websocket.conf移动端地图加载慢未启用离线瓦片在nginx.conf中添加location /tiles/ { add_header Cache-Control public, max-age31536000; }预置瓦片时用wget -r -np -nH --cut-dirs3 -R index.html* https://t0.tianditu.gov.cn/vec_w/最后分享一个小技巧天地图的tk参数其实可以“续命”。当检测到tk即将过期如剩余30秒前端JS可提前发起一次/geocoding/v1/geoconv转换请求不消耗配额天地图服务器会返回新的tk。我们用这个机制实现了tk的无缝续期用户永远感觉不到地图刷新。我在实际项目中发现真正卡住开发者的从来不是技术难点而是对业务场景的理解深度。比如“电子围栏”这个词物业客户要的是“保安巡逻路线是否偏离”物流客户要的是“货车是否按时进入卸货区”教育客户要的是“学生是否走出校园范围”。同一个技术方案必须用不同的业务语言去包装。所以每次接到需求我第一件事不是写代码而是拉着客户画流程图谁在什么时间、什么地点、做什么动作、产生什么数据、触发什么规则。这张图比任何技术文档都重要。