better-staridc-MNBT
1<?php
2/**
3 * realname 插件 - 身份证 / 手机号本地校验算法
4 *
5 * 全程本地算法,不调用任何外部 API:
6 * - 身份证:GB 11643-1999(18 位结构、行政区划码、出生日期、顺序码、校验码)
7 * - 手机号:11 位 + 号段表
8 */
9
10if (!defined('IN_CRONLITE')) {
11 exit;
12}
13
14/**
15 * 身份证号校验(GB 11643-1999)
16 *
17 * @param string $idcard 身份证号
18 * @return array ['ok'=>bool, 'msg'=>string, 'birthday'=>'Y-m-d', 'gender'=>'male|female', 'age'=>int, 'area'=>'省份名']
19 */
20function realname_idcard_validate($idcard)
21{
22 $idcard = strtoupper(trim((string)$idcard));
23 if ($idcard === '') {
24 return ['ok' => false, 'msg' => '身份证号不能为空'];
25 }
26 if (!preg_match('/^\d{17}[\dX]$/', $idcard)) {
27 return ['ok' => false, 'msg' => '身份证号格式不正确(应为 18 位数字,末位可为 X)'];
28 }
29
30 // 2. 行政区划码(前 6 位)
31 $area = realname_idcard_area(substr($idcard, 0, 6));
32 if ($area === '') {
33 return ['ok' => false, 'msg' => '身份证号地区码无效'];
34 }
35
36 // 3. 出生日期(第 7-14 位)
37 $birth = substr($idcard, 6, 8);
38 if (!realname_idcard_check_date($birth)) {
39 return ['ok' => false, 'msg' => '身份证号出生日期无效'];
40 }
41 $birthday = substr($birth, 0, 4) . '-' . substr($birth, 4, 2) . '-' . substr($birth, 6, 2);
42
43 // 4. 顺序码(第 15-17 位)
44 $seq = substr($idcard, 14, 3);
45 if ((int)$seq < 0 || (int)$seq > 999) {
46 return ['ok' => false, 'msg' => '身份证号顺序码无效'];
47 }
48
49 // 5. 校验码(第 18 位)
50 if (!realname_idcard_checksum_ok($idcard)) {
51 return ['ok' => false, 'msg' => '身份证号校验码错误'];
52 }
53
54 $age = realname_idcard_age($birthday);
55 $gender = ((int)$seq % 2 === 1) ? 'male' : 'female';
56
57 return [
58 'ok' => true,
59 'msg' => 'ok',
60 'birthday' => $birthday,
61 'gender' => $gender,
62 'age' => $age,
63 'area' => $area,
64 ];
65}
66
67/**
68 * 校验码算法(GB 11643-1999)
69 * 加权因子与余数映射,返回计算出的校验码字符。
70 */
71function realname_idcard_checksum($idcard)
72{
73 $weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
74 $map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
75 $sum = 0;
76 for ($i = 0; $i < 17; $i++) {
77 $sum += (int)$idcard[$i] * $weights[$i];
78 }
79 return $map[$sum % 11];
80}
81
82/**
83 * 校验末位是否与计算值一致。
84 */
85function realname_idcard_checksum_ok($idcard)
86{
87 return strtoupper($idcard[17]) === realname_idcard_checksum($idcard);
88}
89
90/**
91 * 出生日期真实性校验(含闰年)。
92 */
93function realname_idcard_check_date($ymd)
94{
95 if (!preg_match('/^(\d{4})(\d{2})(\d{2})$/', $ymd, $m)) {
96 return false;
97 }
98 $y = (int)$m[1];
99 $mo = (int)$m[2];
100 $d = (int)$m[3];
101 if ($mo < 1 || $mo > 12 || $d < 1 || $d > 31) {
102 return false;
103 }
104 return checkdate($mo, $d, $y);
105}
106
107/**
108 * 从出生日期计算周岁年龄。
109 */
110function realname_idcard_age($birthday)
111{
112 try {
113 $b = new DateTime($birthday);
114 $now = new DateTime('now');
115 $diff = $now->diff($b);
116 return (int)$diff->y;
117 } catch (Throwable $e) {
118 return 0;
119 }
120}
121
122/**
123 * 手机号校验(11 位 + 号段表)。
124 */
125function realname_phone_validate($phone)
126{
127 $phone = trim((string)$phone);
128 if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
129 return ['ok' => false, 'msg' => '手机号格式不正确(应为 11 位数字)'];
130 }
131 // 号段表:第二位 3-9 已覆盖主流号段;这里再细筛个别保留号段
132 $second = (int)$phone[1];
133 $third = (int)$phone[2];
134 // 14 号段仅 140-149 部分虚拟运营商在网;13/15/16/17/18/19 均合法
135 if ($second === 3 && $third === 0) {
136 return ['ok' => false, 'msg' => '手机号号段无效'];
137 }
138 if ($second === 4 && !in_array($third, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], true)) {
139 return ['ok' => false, 'msg' => '手机号号段无效'];
140 }
141 return ['ok' => true, 'msg' => 'ok'];
142}
143
144/**
145 * 姓名合法性:2~20 个汉字或点号(少数民族姓名可能含点号)。
146 */
147function realname_name_validate($name)
148{
149 $name = trim((string)$name);
150 $len = mb_strlen($name, 'UTF-8');
151 if ($len < 2 || $len > 20) {
152 return ['ok' => false, 'msg' => '姓名长度应为 2~20 个字符'];
153 }
154 if (!preg_match('/^[\x{4e00}-\x{9fa5}·•.]+$/u', $name)) {
155 return ['ok' => false, 'msg' => '姓名只能包含汉字或间隔点'];
156 }
157 return ['ok' => true, 'msg' => 'ok'];
158}
159
160/**
161 * 行政区划码(前 6 位)→ 省份名。
162 * 内置省级前两位映射 + 常用市级前四位映射。
163 * 需覆盖全部省份前 2 位(11-65 之间的已知省区),用于粗校验。
164 */
165function realname_idcard_area($code6)
166{
167 $province_map = [
168 '11' => '北京', '12' => '天津', '13' => '河北', '14' => '山西', '15' => '内蒙古',
169 '21' => '辽宁', '22' => '吉林', '23' => '黑龙江',
170 '31' => '上海', '32' => '江苏', '33' => '浙江', '34' => '安徽', '35' => '福建',
171 '36' => '江西', '37' => '山东',
172 '41' => '河南', '42' => '湖北', '43' => '湖南', '44' => '广东', '45' => '广西',
173 '46' => '海南',
174 '50' => '重庆', '51' => '四川', '52' => '贵州', '53' => '云南', '54' => '西藏',
175 '61' => '陕西', '62' => '甘肃', '63' => '青海', '64' => '宁夏', '65' => '新疆',
176 ];
177 $pre2 = substr($code6, 0, 2);
178 if (!isset($province_map[$pre2])) {
179 return '';
180 }
181 // 前四位市级粗校验:00 结尾为省级单位,非 00 需为非零组合(在此仅校验 0-9)
182 if ((int)substr($code6, 2, 2) < 0 || (int)substr($code6, 2, 2) > 90) {
183 return '';
184 }
185 // 后两位区县码
186 if ((int)substr($code6, 4, 2) < 0 || (int)substr($code6, 4, 2) > 99) {
187 return '';
188 }
189 return $province_map[$pre2];
190}
191
192/**
193 * 掩码工具:身份证号 → 110***********1234
194 */
195function realname_mask_idcard($idcard)
196{
197 $idcard = trim((string)$idcard);
198 if (strlen($idcard) < 8) {
199 return '****';
200 }
201 return substr($idcard, 0, 3) . str_repeat('*', strlen($idcard) - 7) . substr($idcard, -4);
202}
203
204/**
205 * 掩码工具:手机号 → 138****1234
206 */
207function realname_mask_phone($phone)
208{
209 $phone = trim((string)$phone);
210 if (strlen($phone) < 7) {
211 return '****';
212 }
213 return substr($phone, 0, 3) . '****' . substr($phone, -4);
214}
215
216/**
217 * 掩码工具:姓名 → 张* / 李*四(两个字取首字,三字及以上取首尾)
218 */
219function realname_mask_name($name)
220{
221 $name = trim((string)$name);
222 $len = mb_strlen($name, 'UTF-8');
223 if ($len <= 1) {
224 return $name === '' ? '**' : $name;
225 }
226 if ($len === 2) {
227 return mb_substr($name, 0, 1, 'UTF-8') . '*';
228 }
229 return mb_substr($name, 0, 1, 'UTF-8') . str_repeat('*', $len - 2) . mb_substr($name, -1, 1, 'UTF-8');
230}