QinGang_interested/lib/tools/id_cart_util.dart

254 lines
7.0 KiB
Dart
Raw Normal View History

2025-12-12 09:11:30 +08:00
// 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];
}