356 lines
8.4 KiB
Dart
356 lines
8.4 KiB
Dart
import 'dart:core';
|
||
|
||
class IDCardInfo {
|
||
final String raw;
|
||
final bool isValid;
|
||
final String? error;
|
||
final String? id18;
|
||
final String? addressCode;
|
||
final String? provinceCode;
|
||
final String? province;
|
||
final DateTime? birthDate;
|
||
final String? birth;
|
||
final int? age;
|
||
final String? gender;
|
||
final bool checksumValid;
|
||
final String? constellation;
|
||
final String? zodiac;
|
||
|
||
IDCardInfo({
|
||
required this.raw,
|
||
required this.isValid,
|
||
this.error,
|
||
this.id18,
|
||
this.addressCode,
|
||
this.provinceCode,
|
||
this.province,
|
||
this.birthDate,
|
||
this.birth,
|
||
this.age,
|
||
this.gender,
|
||
required this.checksumValid,
|
||
this.constellation,
|
||
this.zodiac,
|
||
});
|
||
|
||
Map<String, dynamic> toJson() => {
|
||
'raw': raw,
|
||
'isValid': isValid,
|
||
'error': error,
|
||
'id18': id18,
|
||
'addressCode': addressCode,
|
||
'provinceCode': provinceCode,
|
||
'province': province,
|
||
'birth': birth,
|
||
'age': age,
|
||
'gender': gender,
|
||
'checksumValid': checksumValid,
|
||
'constellation': constellation,
|
||
'zodiac': zodiac,
|
||
};
|
||
|
||
@override
|
||
String toString() => toJson().toString();
|
||
}
|
||
|
||
/// 只校验 18 位身份证号
|
||
IDCardInfo parseChineseIDCard(String? id) {
|
||
final raw = _normalizeId(id);
|
||
|
||
if (raw.isEmpty) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '身份证号不能为空',
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
if (raw.length != 18) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '身份证号长度不正确,应为18位',
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
if (!RegExp(r'^\d{17}[\dX]$').hasMatch(raw)) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '身份证号格式不正确:前17位必须是数字,最后一位必须是数字或X',
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
const provinceMap = <String, String>{
|
||
'11': '北京市',
|
||
'12': '天津市',
|
||
'13': '河北省',
|
||
'14': '山西省',
|
||
'15': '内蒙古自治区',
|
||
'21': '辽宁省',
|
||
'22': '吉林省',
|
||
'23': '黑龙江省',
|
||
'31': '上海市',
|
||
'32': '江苏省',
|
||
'33': '浙江省',
|
||
'34': '安徽省',
|
||
'35': '福建省',
|
||
'36': '江西省',
|
||
'37': '山东省',
|
||
'41': '河南省',
|
||
'42': '湖北省',
|
||
'43': '湖南省',
|
||
'44': '广东省',
|
||
'45': '广西壮族自治区',
|
||
'46': '海南省',
|
||
'50': '重庆市',
|
||
'51': '四川省',
|
||
'52': '贵州省',
|
||
'53': '云南省',
|
||
'54': '西藏自治区',
|
||
'61': '陕西省',
|
||
'62': '甘肃省',
|
||
'63': '青海省',
|
||
'64': '宁夏回族自治区',
|
||
'65': '新疆维吾尔自治区',
|
||
'71': '台湾省',
|
||
'81': '香港特别行政区',
|
||
'82': '澳门特别行政区',
|
||
'91': '国外',
|
||
};
|
||
|
||
final addressCode = raw.substring(0, 6);
|
||
final provinceCode = raw.substring(0, 2);
|
||
final province = provinceMap[provinceCode];
|
||
|
||
if (province == null) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '地址码不合法:省份码错误',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
final yearStr = raw.substring(6, 10);
|
||
final monthStr = raw.substring(10, 12);
|
||
final dayStr = raw.substring(12, 14);
|
||
|
||
if (!RegExp(r'^(18|19|20)\d{2}$').hasMatch(yearStr)) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '年份不合法:必须以18、19或20开头',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
if (!RegExp(r'^(0[1-9]|1[0-2])$').hasMatch(monthStr)) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '月份不合法:必须在01到12之间',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
if (!RegExp(r'^(0[1-9]|[12]\d|3[01])$').hasMatch(dayStr)) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '日期不合法:必须在01到31之间',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
final year = int.parse(yearStr);
|
||
final month = int.parse(monthStr);
|
||
final day = int.parse(dayStr);
|
||
|
||
DateTime? birthDate;
|
||
try {
|
||
final dt = DateTime(year, month, day);
|
||
if (dt.year == year && dt.month == month && dt.day == day) {
|
||
birthDate = dt;
|
||
}
|
||
} catch (_) {
|
||
birthDate = null;
|
||
}
|
||
|
||
if (birthDate == null) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '出生日期无效:请检查年月日是否真实存在',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
final seqCode = raw.substring(14, 17);
|
||
if (!RegExp(r'^\d{3}$').hasMatch(seqCode)) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '顺序码不合法:第15到17位必须全部为数字',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
birthDate: birthDate,
|
||
birth: _formatDate(birthDate),
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
final seqNum = int.parse(seqCode);
|
||
if (seqNum == 0) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '顺序码不合法:不能为000',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
birthDate: birthDate,
|
||
birth: _formatDate(birthDate),
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
final gender = (seqNum % 2 == 1) ? '男' : '女';
|
||
|
||
final checksumValid = _verifyCheck(raw);
|
||
if (!checksumValid) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '校验位不正确',
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
birthDate: birthDate,
|
||
birth: _formatDate(birthDate),
|
||
age: _calcAge(birthDate),
|
||
gender: gender,
|
||
checksumValid: false,
|
||
constellation: _calcConstellation(birthDate.month, birthDate.day),
|
||
zodiac: _calcChineseZodiac(birthDate.year),
|
||
);
|
||
}
|
||
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: true,
|
||
id18: raw,
|
||
addressCode: addressCode,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
birthDate: birthDate,
|
||
birth: _formatDate(birthDate),
|
||
age: _calcAge(birthDate),
|
||
gender: gender,
|
||
checksumValid: true,
|
||
constellation: _calcConstellation(birthDate.month, birthDate.day),
|
||
zodiac: _calcChineseZodiac(birthDate.year),
|
||
);
|
||
}
|
||
|
||
String _normalizeId(String? id) {
|
||
return (id ?? '')
|
||
.trim()
|
||
.replaceAll(' ', '')
|
||
.replaceAll(' ', '')
|
||
.replaceAll('X', 'X')
|
||
.toUpperCase();
|
||
}
|
||
|
||
String _formatDate(DateTime d) {
|
||
return '${d.year.toString().padLeft(4, '0')}-'
|
||
'${d.month.toString().padLeft(2, '0')}-'
|
||
'${d.day.toString().padLeft(2, '0')}';
|
||
}
|
||
|
||
int _calcAge(DateTime birthDate) {
|
||
final now = DateTime.now();
|
||
var age = now.year - birthDate.year;
|
||
if (now.month < birthDate.month ||
|
||
(now.month == birthDate.month && now.day < birthDate.day)) {
|
||
age -= 1;
|
||
}
|
||
return age;
|
||
}
|
||
|
||
String _calcCheckChar(String id17) {
|
||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||
const checkMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||
|
||
int sum = 0;
|
||
for (var i = 0; i < 17; i++) {
|
||
final n = int.parse(id17[i]);
|
||
sum += n * weights[i];
|
||
}
|
||
return checkMap[sum % 11];
|
||
}
|
||
|
||
bool _verifyCheck(String id18) {
|
||
if (id18.length != 18) return false;
|
||
final id17 = id18.substring(0, 17);
|
||
final expected = _calcCheckChar(id17);
|
||
final actual = id18[17].toUpperCase();
|
||
return expected == actual;
|
||
}
|
||
|
||
String _calcConstellation(int month, int day) {
|
||
const names = [
|
||
'摩羯座',
|
||
'水瓶座',
|
||
'双鱼座',
|
||
'白羊座',
|
||
'金牛座',
|
||
'双子座',
|
||
'巨蟹座',
|
||
'狮子座',
|
||
'处女座',
|
||
'天秤座',
|
||
'天蝎座',
|
||
'射手座'
|
||
];
|
||
const startDays = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22];
|
||
final idx = month - 1;
|
||
if (day < startDays[idx]) {
|
||
return names[(idx + 11) % 12];
|
||
} else {
|
||
return names[idx];
|
||
}
|
||
}
|
||
|
||
String _calcChineseZodiac(int year) {
|
||
const zodiacs = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
|
||
final idx = (year - 1900) % 12;
|
||
final i = idx < 0 ? (idx + 12) % 12 : idx;
|
||
return zodiacs[i];
|
||
} |