254 lines
7.0 KiB
Dart
254 lines
7.0 KiB
Dart
// utils/id_card_util.dart
|
||
import 'dart:core';
|
||
|
||
class IDCardInfo {
|
||
final String raw;
|
||
final bool isValid;
|
||
final String? error;
|
||
final String? id18; // 标准化到18位(若输入15位则自动转换)
|
||
final String? provinceCode;
|
||
final String? province;
|
||
final DateTime? birthDate;
|
||
final String? birth; // YYYY-MM-DD
|
||
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.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,
|
||
'provinceCode': provinceCode,
|
||
'province': province,
|
||
'birth': birth,
|
||
'age': age,
|
||
'gender': gender,
|
||
'checksumValid': checksumValid,
|
||
'constellation': constellation,
|
||
'zodiac': zodiac,
|
||
};
|
||
|
||
@override
|
||
String toString() => toJson().toString();
|
||
}
|
||
|
||
/// 主要调用函数:传入身份证号字符串,返回 IDCardInfo
|
||
IDCardInfo parseChineseIDCard(String id) {
|
||
final raw = (id ?? '').toString().trim().toUpperCase();
|
||
if (raw.isEmpty) {
|
||
return IDCardInfo(raw: raw, isValid: false, error: '空字符串', 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': '国外'
|
||
};
|
||
|
||
// 校验正则:15 位全数字;18 位前17 数字 + 最后一位数字或 X
|
||
final reg15 = RegExp(r'^\d{15}$');
|
||
final reg18 = RegExp(r'^\d{17}[\dX]$');
|
||
|
||
String standardized = raw;
|
||
bool convertedFrom15 = false;
|
||
if (reg15.hasMatch(raw)) {
|
||
// 15 位 -> 转 18 位(在第6位后插入 "19"),然后计算校验位
|
||
final prefix17 = raw.substring(0, 6) + '19' + raw.substring(6);
|
||
final checkChar = _calcCheckChar(prefix17);
|
||
standardized = prefix17 + checkChar;
|
||
convertedFrom15 = true;
|
||
} else if (reg18.hasMatch(raw)) {
|
||
standardized = raw;
|
||
} else {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '身份证格式不正确(不是15位或18位)',
|
||
checksumValid: false,
|
||
);
|
||
}
|
||
|
||
// 取出生日期
|
||
final birthStr = standardized.substring(6, 14); // YYYYMMDD
|
||
final year = int.tryParse(birthStr.substring(0, 4));
|
||
final month = int.tryParse(birthStr.substring(4, 6));
|
||
final day = int.tryParse(birthStr.substring(6, 8));
|
||
|
||
if (year == null || month == null || day == null) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '无法解析出生日期',
|
||
id18: standardized,
|
||
checksumValid: _verifyCheck(standardized),
|
||
);
|
||
}
|
||
|
||
// 校验日期是否真实存在(例如闰年等)
|
||
DateTime? birthDate;
|
||
try {
|
||
birthDate = DateTime(year, month, day);
|
||
// 额外检查同一天
|
||
if (birthDate.year != year || birthDate.month != month || birthDate.day != day) {
|
||
birthDate = null;
|
||
}
|
||
} catch (e) {
|
||
birthDate = null;
|
||
}
|
||
|
||
if (birthDate == null) {
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: false,
|
||
error: '出生日期无效',
|
||
id18: standardized,
|
||
checksumValid: _verifyCheck(standardized),
|
||
);
|
||
}
|
||
|
||
// 年龄计算(按生日是否已过来算)
|
||
final now = DateTime.now();
|
||
int age = now.year - birthDate.year;
|
||
if (now.month < birthDate.month || (now.month == birthDate.month && now.day < birthDate.day)) {
|
||
age -= 1;
|
||
}
|
||
|
||
// 性别:第 17 位(索引 16)为序列码的最后一位,奇数男 偶数女
|
||
final seq = standardized.substring(14, 17); // 3 位序列号
|
||
final seqNum = int.tryParse(seq);
|
||
final gender = (seqNum != null && seqNum % 2 == 1) ? '男' : '女';
|
||
|
||
// 省份
|
||
final provinceCode = standardized.substring(0, 2);
|
||
final province = provinceMap[provinceCode] ?? '未知';
|
||
|
||
// 校验位验证
|
||
final checksumValid = _verifyCheck(standardized);
|
||
|
||
// 星座与生肖
|
||
final constellation = _calcConstellation(birthDate.month, birthDate.day);
|
||
final zodiac = _calcChineseZodiac(birthDate.year);
|
||
|
||
return IDCardInfo(
|
||
raw: raw,
|
||
isValid: true,
|
||
id18: standardized,
|
||
provinceCode: provinceCode,
|
||
province: province,
|
||
birthDate: birthDate,
|
||
birth: '${birthDate.year.toString().padLeft(4, '0')}-${birthDate.month.toString().padLeft(2, '0')}-${birthDate.day.toString().padLeft(2, '0')}',
|
||
age: age,
|
||
gender: gender,
|
||
checksumValid: checksumValid,
|
||
constellation: constellation,
|
||
zodiac: zodiac,
|
||
);
|
||
}
|
||
|
||
// ---- 辅助函数 ----
|
||
|
||
// 计算 17 位前缀的校验码(返回 '0'-'9' 或 'X')
|
||
String _calcCheckChar(String id17) {
|
||
// 权重
|
||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||
// 校验码映射 remainder -> char
|
||
const checkMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||
|
||
int sum = 0;
|
||
for (var i = 0; i < 17; i++) {
|
||
final ch = id17[i];
|
||
final n = int.tryParse(ch) ?? 0;
|
||
sum += n * weights[i];
|
||
}
|
||
final mod = sum % 11;
|
||
return checkMap[mod];
|
||
}
|
||
|
||
// 验证完整 18 位身份证的校验位是否正确
|
||
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];
|
||
// 月份从1开始
|
||
final idx = (month - 1);
|
||
if (day < startDays[idx]) {
|
||
return names[(idx + 11) % 12];
|
||
} else {
|
||
return names[idx];
|
||
}
|
||
}
|
||
|
||
// 计算生肖(中国农历生肖按公历年对照,简化算法)
|
||
String _calcChineseZodiac(int year) {
|
||
const zodiacs = [
|
||
'鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'
|
||
];
|
||
// 1900 是鼠年(可以用任意基准)
|
||
final idx = (year - 1900) % 12;
|
||
final i = idx < 0 ? (idx + 12) % 12 : idx;
|
||
return zodiacs[i];
|
||
}
|