Passed
Push — v6 ( 25b5d7...533a60 )
by 光春
03:05
created

QqWryService::getLocation()   C

Complexity

Conditions 13
Paths 136

Size

Total Lines 112
Code Lines 85

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 85
c 0
b 0
f 0
dl 0
loc 112
rs 5.3806
cc 13
nc 136
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
// +----------------------------------------------------------------------
4
// | ThinkLibrary 6.0 for ThinkPhP 6.0
5
// +----------------------------------------------------------------------
6
// | 版权所有 2017~2020 [ https://www.dtapp.net ]
7
// +----------------------------------------------------------------------
8
// | 官方网站: https://gitee.com/liguangchun/ThinkLibrary
9
// +----------------------------------------------------------------------
10
// | 开源协议 ( https://mit-license.org )
11
// +----------------------------------------------------------------------
12
// | gitee 仓库地址 :https://gitee.com/liguangchun/ThinkLibrary
13
// | github 仓库地址 :https://github.com/GC0202/ThinkLibrary
14
// | gitlab 仓库地址 :https://gitlab.com/liguangchun/thinklibrary
15
// | weixin 仓库地址 :https://git.weixin.qq.com/liguangchun/ThinkLibrary
16
// | huaweicloud 仓库地址 :https://codehub-cn-south-1.devcloud.huaweicloud.com/composer00001/ThinkLibrary.git
17
// | Packagist 地址 :https://packagist.org/packages/liguangchun/think-library
18
// +----------------------------------------------------------------------
19
20
namespace DtApp\ThinkLibrary\service;
21
22
use DtApp\ThinkLibrary\exception\DtaException;
23
use DtApp\ThinkLibrary\Service;
24
use think\App;
25
26
/**
27
 * 纯真数据库
28
 * Class QqWryService
29
 * @package DtApp\ThinkLibrary\service
30
 */
31
class QqWryService extends Service
32
{
33
    /**
34
     * QQWry.Dat文件指针
35
     * @var resource
36
     */
37
    private $fp;
38
39
    /**
40
     * 第一条IP记录的偏移地址
41
     *
42
     * @var int
43
     */
44
    private $firstIp;
45
46
    /**
47
     * 最后一条IP记录的偏移地址
48
     * @var int
49
     */
50
    private $lastIp;
51
52
    /**
53
     * IP记录的总条数(不包含版本信息记录)
54
     * @var int
55
     */
56
    private $totalIp;
57
58
    /**
59
     * 不存在
60
     * @var string
61
     */
62
    private $unknown = '未知';
63
64
    /**
65
     * IP数据库文件存放位置
66
     * @var mixed
67
     */
68
    private $ipPath = '';
69
70
    /**
71
     * 构造函数,打开 QQWry.Dat 文件并初始化类中的信息
72
     * @param App $app
73
     * @throws DtaException
74
     */
75
    public function __construct(App $app)
76
    {
77
        $this->ipPath = config('dtapp.ip_path', '');
78
        if (empty($this->ipPath)) {
79
            throw new DtaException('请检查配置文件是否配置了IP数据库文件存放位置');
80
        }
81
        $this->fp = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type resource of property $fp.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
82
        if (($this->fp = fopen($this->ipPath . 'qqwry.dat', 'rb')) !== false) {
83
            $this->firstIp = $this->getLong();
84
            $this->lastIp = $this->getLong();
85
            $this->totalIp = ($this->lastIp - $this->firstIp) / 7;
86
        }
87
        parent::__construct($app);
88
    }
89
90
    /**
91
     * 设置未知的返回字段
92
     * @param string $unknown
93
     * @return QqWryService
94
     */
95
    public function setUnknown(string $unknown = '未知')
96
    {
97
        $this->unknown = $unknown;
98
        return $this;
99
    }
100
101
    /**
102
     * 获取省信息
103
     * @param string $ip
104
     * @return mixed
105
     * @throws DtaException
106
     */
107
    public function getProvince(string $ip = '')
108
    {
109
        return $this->getLocation($ip)['state'];
110
    }
111
112
    /**
113
     * 获取城市信息
114
     * @param string $ip
115
     * @return mixed
116
     * @throws DtaException
117
     */
118
    public function getCity(string $ip = '')
119
    {
120
        return $this->getLocation($ip)['city'];
121
    }
122
123
    /**
124
     * 获取地区信息
125
     * @param string $ip
126
     * @return mixed
127
     * @throws DtaException
128
     */
129
    public function getArea(string $ip = '')
130
    {
131
        return $this->getLocation($ip)['area'];
132
    }
133
134
    /**
135
     * 获取运营商信息
136
     * @param string $ip
137
     * @return mixed
138
     * @throws DtaException
139
     */
140
    public function getExtend(string $ip = '')
141
    {
142
        return $this->getLocation($ip)['extend'];
143
    }
144
145
    /**
146
     * 根据所给 IP 地址或域名返回所在地区信息
147
     * @param string $ip
148
     * @return mixed|null
149
     * @throws DtaException
150
     */
151
    public function getLocation(string $ip = '')
152
    {
153
        if (empty($ip)) {
154
            $ip = get_ip();
155
        }
156
        if (strpos($ip, 'http://') === 0) {
157
            $ip = substr($ip, 7);
158
            $ip = gethostbyname($ip);
159
        }
160
        static $locationData = [];
161
        if (!isset($locationData[$ip])) {
162
            if (!$this->fp) {
163
                // 如果数据文件没有被正确打开,则直接返回错误
164
                throw new DtaException('数据库文件不存在!');
165
            }
166
            $location['ip'] = $ip;   // 将输入的域名转化为IP地址
0 ignored issues
show
Comprehensibility Best Practice introduced by
$location was never initialized. Although not strictly required by PHP, it is generally a good practice to add $location = array(); before regardless.
Loading history...
167
            $ip = $this->packIp($location['ip']);   // 将输入的IP地址转化为可比较的IP地址
168
            // 不合法的IP地址会被转化为255.255.255.255
169
            // 对分搜索
170
            $l = 0;                         // 搜索的下边界
171
            $u = $this->totalIp;            // 搜索的上边界
172
            $findip = $this->lastIp;        // 如果没有找到就返回最后一条IP记录(QQWry.Dat的版本信息)
173
            while ($l <= $u) {              // 当上边界小于下边界时,查找失败
174
                $i = floor(($l + $u) / 2);  // 计算近似中间记录
175
                fseek($this->fp, $this->firstIp + $i * 7);
0 ignored issues
show
Bug introduced by
$this->firstIp + $i * 7 of type double is incompatible with the type integer expected by parameter $offset of fseek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

175
                fseek($this->fp, /** @scrutinizer ignore-type */ $this->firstIp + $i * 7);
Loading history...
176
                $beginip = strrev(fread($this->fp, 4));     // 获取中间记录的开始IP地址
177
                // strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式
178
                // 以便用于比较,后面相同。
179
                if ($ip < $beginip) {       // 用户的IP小于中间记录的开始IP地址时
180
                    $u = $i - 1;            // 将搜索的上边界修改为中间记录减一
181
                } else {
182
                    fseek($this->fp, $this->getLong3());
183
                    $endip = strrev(fread($this->fp, 4));   // 获取中间记录的结束IP地址
184
                    if ($ip > $endip) {     // 用户的IP大于中间记录的结束IP地址时
185
                        $l = $i + 1;        // 将搜索的下边界修改为中间记录加一
186
                    } else {                  // 用户的IP在中间记录的IP范围内时
187
                        $findip = $this->firstIp + $i * 7;
188
                        break;              // 则表示找到结果,退出循环
189
                    }
190
                }
191
            }
192
            //获取查找到的IP地理位置信息
193
            fseek($this->fp, $findip);
194
            $location['beginip'] = long2ip($this->getLong());   // 用户IP所在范围的开始地址
195
            $offset = $this->getLong3();
196
            fseek($this->fp, $offset);
197
            $location['endip'] = long2ip($this->getLong());     // 用户IP所在范围的结束地址
198
            $byte = fread($this->fp, 1);    // 标志字节
199
            switch (ord($byte)) {
200
                case 1:                     // 标志字节为1,表示国家和区域信息都被同时重定向
201
                    $countryOffset = $this->getLong3();         // 重定向地址
202
                    fseek($this->fp, $countryOffset);
203
                    $byte = fread($this->fp, 1);    // 标志字节
204
                    switch (ord($byte)) {
205
                        case 2:             // 标志字节为2,表示国家信息又被重定向
206
                            fseek($this->fp, $this->getLong3());
207
                            $location['all'] = $this->getString();
208
                            fseek($this->fp, $countryOffset + 4);
209
                            $location['extend'] = $this->getExtendString();
210
                            break;
211
                        default:            // 否则,表示国家信息没有被重定向
212
                            $location['all'] = $this->getString($byte);
213
                            $location['extend'] = $this->getExtendString();
214
                            break;
215
                    }
216
                    break;
217
                case 2:                     // 标志字节为2,表示国家信息被重定向
218
                    fseek($this->fp, $this->getLong3());
219
                    $location['all'] = $this->getString();
220
                    fseek($this->fp, $offset + 8);
221
                    $location['extend'] = $this->getExtendString();
222
                    break;
223
                default:                    // 否则,表示国家信息没有被重定向
224
                    $location['all'] = $this->getString($byte);
225
                    $location['extend'] = $this->getExtendString();
226
                    break;
227
            }
228
            // CZ88.NET表示没有有效信息
229
            if (trim($location['all']) === 'CZ88.NET') {
230
                $location['all'] = $this->unknown;
231
            }
232
            if (trim($location['extend']) === 'CZ88.NET') {
233
                $location['extend'] = '';
234
            }
235
            $location['all'] = iconv("gb2312", "UTF-8//IGNORE", $location['all']);
236
            $location['extend'] = iconv("gb2312", "UTF-8//IGNORE", $location['extend']);
237
            $location['extend'] = $location['extend'] ?? '';
238
            $parseData = $this->parseLocation($location['all']);
239
            $location['state'] = $parseData[0];
240
            $location['city'] = $parseData[1];
241
            $location['area'] = $parseData[2];
242
243
            // 全部地址
244
            $res['location_all'] = $location['all'];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$res was never initialized. Although not strictly required by PHP, it is generally a good practice to add $res = array(); before regardless.
Loading history...
245
            // 运营商
246
            $res['isp']['name'] = $location['extend'];
247
            // IP
248
            $res['ip']['ipv4'] = $location['ip'];
249
            $res['ip']['beginip'] = $location['beginip'];
250
            $res['ip']['endip'] = $location['endip'];
251
            $res['ip']['trueip'] = ip2long($location['ip']);
252
            $res['ip']['ipv6'] = $this->getNormalizedIP($location['ip']);
253
            $getAdCodeLatLng = $this->getNameAdCodeLatLng($location['state'], $location['city'], $location['area']);
254
            // 省份
255
            $res['province'] = $getAdCodeLatLng['province'];
256
            // 城市
257
            $res['city'] = $getAdCodeLatLng['city'];
258
            // 地区
259
            $res['district'] = $getAdCodeLatLng['district'];
260
            $locationData[$ip] = $location;
261
        }
262
        return $res;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $res does not seem to be defined for all execution paths leading up to this point.
Loading history...
263
    }
264
265
    /**
266
     * 解析省市区县
267
     * @param $location
268
     * @return array
269
     * @example '江苏省苏州市吴江市' , '江苏省苏州市吴中区' , '江苏省苏州市昆山市' , '黑龙江省鸡西市' , '广西桂林市' , '陕西省西安市户县' , '河南省开封市通许县' ,'内蒙古呼伦贝尔市海拉尔区','甘肃省白银市平川区','孟加拉','上海市' , '北京市朝阳区' ,'美国' ,'香港' ,  俄罗斯' ,'IANA'
270
     */
271
    private function parseLocation($location)
272
    {
273
        $state = $city = $area = $this->unknown;
274
        if (preg_match('/^(.+省)?(新疆|内蒙古|宁夏|西藏|广西|香港|澳门)?(.+市)?(.+市)?(.+(县|区))?/', $location, $preg)) {
275
            if (count($preg) == 4) {        //匹配 "浙江省杭州市"
276
                $state = $preg[1] ? $preg[1] : ($preg[2] ? $preg[2] : $preg[3]);
277
                $city = $preg[3];
278
            } else if (count($preg) == 7) { //匹配 "浙江省杭州市江干区"
279
                $state = $preg[1] ? $preg[1] : ($preg[2] ? $preg[2] : $preg[3]);
280
                $city = $preg[3];
281
                $area = $preg[5];
282
            } else if (count($preg) == 3) { //匹配 "香港"
283
                $state = $preg[1] ? $preg[1] : $preg[2];
284
                $city = $state;
285
            } else if (count($preg) == 2) {  //匹配 "浙江省"
286
                $state = $preg[1] ? $preg[1] : $this->unknown;
287
            }
288
        }
289
        return [$state, $city, $area];
290
    }
291
292
    /**
293
     * 返回读取的长整型数
294
     * @return mixed
295
     */
296
    private function getLong()
297
    {
298
        //将读取的little-endian编码的4个字节转化为长整型数
299
        $result = unpack('Vlong', fread($this->fp, 4));
300
        return $result['long'];
301
    }
302
303
    /**
304
     * 返回读取的3个字节的长整型数
305
     * @return mixed
306
     */
307
    private function getLong3()
308
    {
309
        //将读取的little-endian编码的3个字节转化为长整型数
310
        $result = unpack('Vlong', fread($this->fp, 3) . chr(0));
311
        return $result['long'];
312
    }
313
314
    /**
315
     * 返回压缩后可进行比较的IP地址
316
     * @param $ip
317
     * @return false|string
318
     */
319
    private function packIp($ip)
320
    {
321
        // 将IP地址转化为长整型数,如果在PHP5中,IP地址错误,则返回False,
322
        // 这时intval将Flase转化为整数-1,之后压缩成big-endian编码的字符串
323
        return pack('N', intval(ip2long($ip)));
324
    }
325
326
    /**
327
     * 返回读取的字符串
328
     *
329
     * @access private
330
     * @param string $data
331
     * @return string
332
     */
333
    private function getString($data = "")
334
    {
335
        $char = fread($this->fp, 1);
336
        while (ord($char) > 0) {        // 字符串按照C格式保存,以\0结束
337
            $data .= $char;             // 将读取的字符连接到给定字符串之后
338
            $char = fread($this->fp, 1);
339
        }
340
        return $data;
341
    }
342
343
    /**
344
     * 返回地区信息
345
     * @return string
346
     */
347
    private function getExtendString()
348
    {
349
        $byte = fread($this->fp, 1);    // 标志字节
350
        switch (ord($byte)) {
351
            case 0:                     // 没有区域信息
352
                $area = "";
353
                break;
354
            case 1:
355
            case 2:                     // 标志字节为1或2,表示区域信息被重定向
356
                fseek($this->fp, $this->getLong3());
357
                $area = $this->getString();
358
                break;
359
            default:                    // 否则,表示区域信息没有被重定向
360
                $area = $this->getString($byte);
361
                break;
362
        }
363
        return $area;
364
    }
365
366
    /**
367
     * 析构函数,用于在页面执行结束后自动关闭打开的文件。
368
     */
369
    public function __destruct()
370
    {
371
        if ($this->fp) {
372
            fclose($this->fp);
373
        }
374
        $this->fp = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type resource of property $fp.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
375
    }
376
377
    /**
378
     * ipv4转换ipv6
379
     * @param $ip
380
     * @return bool|false|string|string[]|null
381
     */
382
    protected function getNormalizedIP($ip)
383
    {
384
        if (!is_string($ip)) {
385
            return '';
386
        }
387
        if (preg_match('%^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$%', $ip, $match)) {
388
            $IPParts = array();
389
            for ($i = 1; $i <= 4; $i++) {
390
                $IPPart = (int)$match[$i];
391
                if ($IPPart > 255) {
392
                    return '';
393
                }
394
                $IPParts[$i] = str_pad(decHex($IPPart), 2, '0', STR_PAD_LEFT);
395
            }
396
            return '0000:0000:0000:0000:0000:ffff:' . $IPParts[1] . $IPParts[2] . ':' . $IPParts[3] . $IPParts[4];
397
        }
398
        return '';
399
    }
400
401
    /**
402
     * 解析CODE
403
     * @param $province_name
404
     * @param $city_name
405
     * @param $district_name
406
     * @return array
407
     */
408
    private function getNameAdCodeLatLng($province_name, $city_name, $district_name)
409
    {
410
        // 名称
411
        $province['name'] = $province_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$province was never initialized. Although not strictly required by PHP, it is generally a good practice to add $province = array(); before regardless.
Loading history...
412
        $city['name'] = $city_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$city was never initialized. Although not strictly required by PHP, it is generally a good practice to add $city = array(); before regardless.
Loading history...
413
        $district['name'] = $district_name;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$district was never initialized. Although not strictly required by PHP, it is generally a good practice to add $district = array(); before regardless.
Loading history...
414
        // adcode
415
        $province['adcode'] = '';
416
        $city['adcode'] = '';
417
        $district['adcode'] = '';
418
        // lat
419
        $province['lat'] = '';
420
        $city['lat'] = '';
421
        $district['lat'] = '';
422
        // lng
423
        $province['lng'] = '';
424
        $city['lng'] = '';
425
        $district['lng'] = '';
426
427
        if (!empty($province_name)) {
428
            $json_province = json_decode(file_get_contents($this->ipPath . 'province.json'), true);
429
            foreach ($json_province['rows'] as $key => $value) {
430
                if ($value['name'] == $province_name) {
431
                    $province['name'] = $value['name'];
432
                    $province['adcode'] = $value['adcode'];
433
                    $province['lat'] = $value['lat'];
434
                    $province['lng'] = $value['lng'];
435
                }
436
            }
437
        }
438
        if (!empty($city_name)) {
439
            $json_city = json_decode(file_get_contents($this->ipPath . 'city.json'), true);
440
            foreach ($json_city['rows'] as $key => $value) {
441
                if ($value['name'] == $city_name) {
442
                    $city['name'] = $value['name'];
443
                    $city['adcode'] = $value['adcode'];
444
                    $city['lat'] = $value['lat'];
445
                    $city['lng'] = $value['lng'];
446
                }
447
            }
448
        }
449
        if (!empty($district_name)) {
450
            $json_district = json_decode(file_get_contents($this->ipPath . 'district.json'), true);
451
            foreach ($json_district['rows'] as $key => $value) {
452
                if ($value['name'] == $district_name) {
453
                    $district['name'] = $value['name'];
454
                    $district['adcode'] = $value['adcode'];
455
                    $district['lat'] = $value['lat'];
456
                    $district['lng'] = $value['lng'];
457
                }
458
            }
459
        }
460
        return [
461
            'province' => $province,
462
            'city' => $city,
463
            'district' => $district
464
        ];
465
    }
466
}
467