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 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 = { '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]; }