固定电话验证详解:区号、号码、分机号的完整验证
📋 目录
- 引言
- 一、固定电话的三段式结构
- 二、区号、号码、分机号的真实规则
- 三、最容易写错的"伪正则"
- 四、@ChineseLandline:开箱即用的固话验证
- [五、场景选型:固话 / 手机 / 二选一](#五、场景选型:固话 / 手机 / 二选一 "#%E4%BA%94%E5%9C%BA%E6%99%AF%E9%80%89%E5%9E%8B%E5%9B%BA%E8%AF%9D--%E6%89%8B%E6%9C%BA--%E4%BA%8C%E9%80%89%E4%B8%80")
- 六、@PhoneNumber:面向国际的分机号支持
- [七、链式 API:不写 DTO 也能验](#七、链式 API:不写 DTO 也能验 "#%E4%B8%83%E9%93%BE%E5%BC%8F-api%E4%B8%8D%E5%86%99-dto-%E4%B9%9F%E8%83%BD%E9%AA%8C")
- 八、源码解读:宽松背后的两条路径
- 九、边界与坑
- 总结
- 项目地址
引言
移动互联网时代,表单里还有多少地方要填固定电话?
企业通讯录、客服回访、政务办事、银行开户、公司招聘......"座机"字段依然无处不在。它比手机号更难校验------手机号规则简单统一(11 位、1 开头),而固定电话要同时面对三件不确定的事:
| 组成 | 变化 |
|---|---|
| 区号 | 带不带 0?3 位还是 4 位? |
| 号码 | 7 位还是 8 位?大城市小城市不一样 |
| 分机 | 有没有?几位?用什么分隔符? |
手写正则时,这三件事任意一个考虑不周就会误杀合法号码或放行垃圾输入。ValidX 把这条规则封装成了 @ChineseLandline,本文拆解它背后的完整规则,并对比 @ChinesePhone、@ChinesePhoneOrLandline、@PhoneNumber 的选型边界。
一、固定电话的三段式结构
先看一段真实的中国大陆固话号码:
yaml
010 - 12345678 - 1234
└┬─┘ └──┬───┘ └┬─┘
区号 号码 分机(可选)
- 区号 :以
0开头(长途冠码)+ 城市代码,共 3~4 位 - 号码 :本地局号 + 用户号,共 7~8 位
- 分机 :企业内部话务台转接,1~6 位,可选
这三段拼在一起,对应 ValidX ChineseLandlineValidator 里的核心正则:
ruby
^(0\d{2,3}[-\s]?)?\d{7,8}([-\s]\d{1,6})?$
逐段拆开看:
| 正则片段 | 含义 | 真实对应 |
|---|---|---|
(0\d{2,3}[-\s]?)? |
区号,整体可选(支持不填区号的本地号码) | 010-、0755-、0755 |
\d{7,8} |
本地号码 7~8 位 | 大城市 8 位、中小城市 7 位 |
([-\s]\d{1,6})? |
分机号 1~6 位,整体可选 | -1234、 100 |
正则还容忍两类分隔符:- 和空格([-\s]),且区号与号码之间允许没有分隔符 ------01012345678 这种紧凑写法也能通过。
二、区号、号码、分机号的真实规则
① 区号:0 是长途冠码,不是区号的一部分
很多人把"区号"理解成 010、0755 整体,但电信标准里 0 是长途冠码,真正的城市代码是它后面的 1~3 位:
| 城市 | 完整区号 | 拆解 | 位数 |
|---|---|---|---|
| 北京 | 010 |
0 + 10 |
3 位 |
| 上海 | 021 |
0 + 21 |
3 位 |
| 广州 | 020 |
0 + 20 |
3 位 |
| 深圳 | 0755 |
0 + 755 |
4 位 |
| 成都 | 028 |
0 + 28 |
3 位 |
| 长沙 | 0731 |
0 + 731 |
4 位 |
对应正则 0\d{2,3}:第一位必须是 0 ,后面跟 2~3 位城市代码。这就是为什么 400-xxx-xxxx、800-xxx-xxxx 这类客服号码会被拒绝------它们不以 0 开头,不是区号体系。
② 本地号码:7 位还是 8 位,随城市容量走
本地号码由"局号 + 用户号"组成:直辖市、省会等大容量城市升到 8 位,中小城市多为 7 位。正则用 \d{7,8} 同时容纳两者------这也是很多手写正则写死 \d{8} 后误杀小城市电话的原因。
③ 分机号:企业内部编号,1~6 位
分机是 PBX(用户交换机)内部的分支编号,形如 -1234 或空格分隔的 100。ValidX 分机规则是 \d{1,6},最长 6 位,前缀 - 或空格。
④ 分隔符:- / 空格 / 无
区号与号码之间三种写法都合法:
yaml
010-12345678 0755 12345678 01012345678 ✓ 都通过
三、最容易写错的"伪正则"
网上流传的固话正则,几乎每条都踩过下面某个坑:
| 常见写法 | 问题 | 后果 |
|---|---|---|
\d{3,4}-\d{7,8} |
区号不强制 0 开头 |
400-123-4567、80012345678 全被放行 |
0\d{3}-\d{8} |
锁死 4+8 结构 | 北京 010 + 7 位号码被误杀,小城市 7 位号全灭 |
0\d{2,3}-\d{7,8} |
不收分机、不收空格/紧凑写法 | 带分机的公司电话、无分隔符的输入被拒 |
0\d{2,3}-\d{7,8}(-\d+)? |
分机位数无上限 | -9999999999 也能过 |
^0\d{2,3}-?\d{7,8}$ |
只认区号,不认本地号码 | 内线直拨的 7 位号被拒 |
一条合格的中国大陆固话规则,至少要同时满足:区号可选且 0 开头 34 位、本地号码 7 8 位、分机可选 1~6 位、分隔符为 -/空格/无 ------这正是 @ChineseLandline 的完整语义。
四、@ChineseLandline:开箱即用的固话验证
公司通讯录场景,验证一条联系人记录:
java
public class ContactDTO {
@NotNull
private String name;
@NotBlank
@ChineseLandline // 固定电话号码格式不正确
private String officePhone; // 公司座机:010-12345678 / 010-12345678-1234
@NotBlank
@ChinesePhone // 手机号码格式不正确
private String mobile;
}
@ChineseLandline 与其它 ValidX 注解一致:
- 注解位于
io.github.vipxieliang.validx.annotations,验证器为ChineseLandlineValidator - 默认消息
{io.github.vipxieliang.validx.annotation.chinese.landline}→ "固定电话号码格式不正确" - 中文、英文、日文等 9 种语言消息文件内置,无需手动配 i18n
- 支持
groups、message覆盖:@ChineseLandline(message = "座机号格式不对,示例:010-12345678")
实测通过的样例:
| 输入 | 结果 |
|---|---|
010-12345678(北京,8 位号) |
✅ |
021-1234567(上海,7 位号) |
✅ |
0755-12345678(深圳,4 位区号) |
✅ |
01012345678(无分隔符) |
✅ |
010-12345678-1234(带分机) |
✅ |
010 12345678 100(空格分隔 + 分机) |
✅ |
12345678(无区号的本地号码) |
✅ |
13812345678(手机号) |
❌ |
400-123-4567(客服号码,非区号体系) |
❌ |
(010)12345678(括号,不支持的格式) |
❌ |
138-1234-5678(手机号加分隔符) |
❌ |
五、场景选型:固话 / 手机 / 二选一
很多字段是"不知道用户填手机还是座机",直接上 @ChineseLandline 会误伤手机号。ValidX 三个注解覆盖三种诉求:
| 注解 | 放行 | 适用场景 |
|---|---|---|
@ChinesePhone |
仅手机号 | 明确要求手机:验证码、登录绑定 |
@ChineseLandline |
仅固定电话 | 明确要求座机:公司电话、政务专线 |
@ChinesePhoneOrLandline |
手机或座机都行 | "联系电话"等开放字段 |
"收货人联系电话"这类字段是典型的二选一,两个正则都会写错的人尤其多------ValidX 直接提供组合注解:
java
@NotBlank
@ChinesePhoneOrLandline // 手机号码或者固定电话号码格式不正确
private String contactPhone;
ChinesePhoneOrLandlineValidator 的实现就是"手机验证器 OR 固话验证器",职责清晰、互不干扰:
java
return phoneValidator.isValid(value, null) || landlineValidator.isValid(value, null);
六、@PhoneNumber:面向国际的分机号支持
如果业务面向外贸、跨国企业(联系人留 +1-555-123-4567),@PhoneNumber 是更合适的注解,它把"分机号"当一等公民支持:
| 参数 | 默认 | 作用 |
|---|---|---|
countryCode |
""(不限国家) |
限定国家代码,如 "+86"、"+1" |
allowExtension |
true |
是否允许分机号 |
strict |
false |
true 时强制 + 开头的国际格式 |
分机关键字支持 ext.、ext、x、#、extension:
java
@PhoneNumber(allowExtension = true)
private String phone; // 合法示例:+1-555-123-4567 ext. 100
用法对比------区分"中国区号固话"与"国际电话":
java
// 外贸联系人:必须 + 开头、带美国区号、不要分机
@PhoneNumber(countryCode = "+1", strict = true, allowExtension = false)
private String usPhone;
// 国内联系人:严格按中国区号 + 固话结构
@ChineseLandline
private String cnPhone;
选型建议 :字段面向中国大陆(表单、电商、政务)用 @ChineseLandline / @ChinesePhoneOrLandline;面向国际客户、需要 + 国家代码与分机关键字的场景才用 @PhoneNumber。两者一"地"一"洋",别混用。
七、链式 API:不写 DTO 也能验
没有 DTO 的接口(比如校验一个前端随手传来的查询参数),可以走链式 API,规则与注解完全等价:
java
ValidX v = ValidX.init();
// 单个固话校验
v.isChineseLandline(officePhone)
.isChinesePhoneOrLandline(contactPhone); // 手机/座机二选一
// 国际电话:指定国家代码 + 关闭分机
v.isPhoneNumber(usPhone, "+1", false, true);
if (v.passed()) {
contactService.save(officePhone, contactPhone, usPhone);
} else {
// v.getErrors() 收集全部失败原因
throw new BusinessException(v.getErrors().get(0));
}
链式方法签名与注解参数一一对应:
| 注解 | 链式方法 |
|---|---|
@ChineseLandline |
isChineseLandline(Object) |
@ChinesePhoneOrLandline |
isChinesePhoneOrLandline(Object) |
@PhoneNumber |
isPhoneNumber(Object) / isPhoneNumber(value, countryCode) / (value, countryCode, allowExtension) / (value, countryCode, allowExtension, strict) |
八、源码解读:宽松背后的两条路径
ChineseLandlineValidator 的 isValid 只做了两件事,先看核心正则:
java
private static final Pattern LANDLINE_PATTERN = Pattern.compile(
"^(0\\d{2,3}[-\\s]?)?\\d{7,8}([-\\s]\\d{1,6})?$"
);
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 空值处理交给@NotNull等其他注解处理
}
// 去除空格和横线后进行验证
String cleanValue = value.replaceAll("[\\s-]", "");
// 检查是否包含非数字字符(除了开头的0)
if (!cleanValue.matches("^0?\\d+$")) {
return false;
}
// 对于固定电话,检查长度是否符合要求
if (cleanValue.length() >= 10 && cleanValue.length() <= 13 && cleanValue.startsWith("0")) {
// 如果去除分隔符后符合固定电话长度要求,认为是有效的
return true;
}
// 验证固定电话(使用原始值,但允许空格和横线)
return LANDLINE_PATTERN.matcher(value).matches();
}
判断分两条路径:
- 宽松路径(无分隔符输入) :先把
-/空格全删掉,若剩下的是0开头、10~13 位的纯数字,直接放行 ------目的是容忍"用户没加分隔符"的输入(如01012345678),不必再精确切分哪几位是区号。 - 正则路径(原始输入) :走
LANDLINE_PATTERN,严格匹配"区号 + 号码(+ 分机)"结构。
两条路径分工的结果是:带分隔符的输入交给正则精判,不带分隔符的输入交给长度粗判。
九、边界与坑
1. 宽松路径比正则有更宽的误放
长度粗判存在两个"其实不该过"的样例(实测确认):
yaml
0755-123456 → 号码只有 6 位,但因去分隔后为 10 位且 0 开头被放行
0101234567890 → 0 开头 13 位任意数字也被放行(真实区号+号码最长 12 位)
如果你对格式要求苛刻(如银行开户号码必须严格符合区号长度表),可以另加一层业务校验兜底,或与服务端"区号-城市"对照表交叉核对。
2. Javadoc 示例言过其实:双分机其实不支持
ChineseLandline 注解的 Javadoc 声称支持 010-12345678-1234-1234(两段分机),但真实正则 ([-\s]\d{1,6})? 只允许一段 分机,实测 010-12345678-1234-1234 被拒绝。企业跨多级总机的两段分机需求目前需自行扩展(如去掉末尾 - 后分段校验),这是注解文档与实现不一致的一处遗留问题,使用时以本文实测为准。
3. 本地号码不以 0/1 开头?真实正则不保证
不少资料称"本地号码不以 0、1 开头",但 \d{7,8} 并不排除这种局向号------ValidX 选择宽松策略,接受此类输入。极端业务要求(局号白名单)需另配服务端校验。
4. 空值放行,必填交给 @NotBlank
与 ValidX 所有注解一致,@ChineseLandline 对 null/空串返回合法------"是否必填"是 @NotBlank/@NotNull 的职责,两者分开才是声明式校验的正确姿势。
5. 客服号码、手机号不会误入
400/800 开头(非 0 区号体系)、手机号(1 开头 11 位)都被正确拒绝。需要"手机或座机都行"请用 @ChinesePhoneOrLandline,而不是放宽成"11 位数字都过"的自写正则。
6. 存库前统一格式
固话允许 -/空格/无分隔三种写法,同一号码可能以三种形态入库------与银行卡号同理,存库前统一为一种格式 (如 010-12345678),并建议把分机号拆成独立字段,避免"格式化整串"时误伤分机。
总结
| 场景 | 推荐注解 | 要点 |
|---|---|---|
| 明确座机 | @ChineseLandline |
区号可选、0 开头 3 |
| 手机或座机 | @ChinesePhoneOrLandline |
手机验证器 OR 固话验证器 |
| 国际电话 | @PhoneNumber |
countryCode / allowExtension / strict,分机支持 ext./x/# |
| 无 DTO 场景 | isChineseLandline / isPhoneNumber(...) |
链式 API 与注解等价 |
固定电话验证的复杂度不在"写正则",而在记住每个不确定点 :区号带不带 0、3 位还是 4 位,号码 7 位还是 8 位,分机有没有、用 - 还是空格、最多几位。把这套规则交给 @ChineseLandline,把"手滑多写一位区号""漏了分机段"这类 bug 从业务代码里彻底移除------这也是 ValidX 把规则标准化、沉淀成注解的意义所在。
项目地址
- GitHub :github.com/vipxieliang...
- Gitee :gitee.com/vipxieliang...
- Maven Central :central.sonatype.com/artifact/io...