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