diff --git a/docs/KYC/KYCConfig.md b/docs/KYC/KYCConfig.md new file mode 100644 index 0000000..602d571 --- /dev/null +++ b/docs/KYC/KYCConfig.md @@ -0,0 +1,236 @@ +# KYC 配置分析文档 + +## 概述 +本文档基于系统代码实际使用情况,分析 `appsettings.json` 文件中 `KYCConfig` 节点下各个配置项的作用和功能。 + +## 配置结构与代码使用分析 + +### 1. XiangyunAPI 配置 +**使用位置**: `XiangyunService.cs` +**作用**: 配置祥云 OCR 识别服务的 API 接口参数 + +```csharp +// 在 XiangyunService 构造函数中使用 +_xiangyunConfig = appConfig.Value.KYCConfig.XiangyunAPI; +``` + +**配置项说明**: +- **BaseUrl**: 祥云 OCR 服务的基础 URL (`https://netocr.com`) +- **Key**: API 访问密钥,用于身份验证 +- **Secret**: API 访问密码,用于身份验证 +- **Timeout**: HTTP 请求超时时间(秒),默认 30 秒 + +**实际用途**: +- 身份证 OCR 识别 (`OCRIDCard`) +- 营业执照 OCR 识别 (`OCRBusinessLicense`) +- 文档 OCR 识别 (`OCRDocument`) +- 表格 OCR 识别 (`OCRTable`) +- 身份证实名验证 (`ValidateIDCard`) +- 护照验证 (`ValidatePassport`) +- 征信查询 (`ValidateCredit`) + +### 2. CertificateCodeMappings 配置 +**使用位置**: `XiangyunCartTypeCodeConverter.cs` +**作用**: 定义 AML 系统内部证件代码与祥云 OCR 服务证件代码的映射关系 + +```csharp +// 获取映射配置 +return _configuration.GetSection("AppConfig:KYCConfig:XiangyunAPI:CertificateCodeMappings") + .Get>(); + +// 转换方法 +public string ToOCRCode(string amlCode) // AML代码转OCR代码 +public string ToCreditCode(string amlCode) // AML代码转征信代码 +public string ToValidateCode(string amlCode) // AML代码转验证代码 +``` + +**映射字段**: +- **AMLCode**: AML 系统内部证件代码(支持多个代码用 "|" 分隔) +- **OCRCode**: 祥云 OCR 服务对应的证件类型代码 +- **CreditCode**: 征信查询使用的代码 +- **ValidateCode**: 验证服务使用的代码 +- **Remark**: 证件类型说明 + +### 3. CertificateEntityColumParseRules 配置 +**使用位置**: `CertificateEntityValueFinder.cs` +**作用**: 定义从 OCR 识别结果中解析实体字段的规则,用于前端UI可以自动填入 + +```csharp +// 获取解析规则 +var rules = _appConfig.Value.KYCConfig.CertificateEntityColumParseRules; + +// 查找匹配规则并提取值 +public string FindValue(string name, CardInfo cardInfo) +public string FindValue(string name, string jsonContent) +``` + +**规则结构**: +- **AMLCode**: 适用的证件代码("*" 表示所有证件) +- **EntityColumn**: 目标实体字段名(如 Name、Number、Birthday 等) +- **EntityColumnNames**: OCR 识别结果中可能的字段名称列表 + +**支持的实体字段**: +- CardName, Name, Number, CountryCode, Birthday, Address, Sex, ExpireDate, FirstOrGivenName, Surname + +### 4. SupportCertificate 配置 +**使用位置**: `KYCService.cs`, `ConsumerPortalService.cs` +**作用**: 定义系统支持的证件类型列表,用于前端显示 + +```csharp +// 获取个人证件类型 +var supportCertificates = _configuration + .GetSection("AppConfig:KYCConfig:SupportCertificate:Individual") + .Get>(); + +// 获取企业证件类型 +var supportCertificateCodes = _configuration + .GetSection("AppConfig:KYCConfig:SupportCertificate:Organization") + .Get>(); +``` + +**用途**: +- 为前端提供可选的证件类型列表 +- 支持多语言显示(英文、简体中文、繁体中文、日文) +- 控制证件类型的启用状态和显示顺序 + +### 5. TranslateMappings 配置 +**使用位置**: `TranslateMapper.cs` +**作用**: 定义验证结果的多语言翻译映射 + +```csharp +// 获取翻译映射 +var translateMappings = _appConfig.Value.KYCConfig.TranslateMappings; + +// 翻译方法 +public string FindValue(string content, string objectType = "") +public MultipleLang FindMultipleLang(string content, string objectType = "") +``` + +**翻译对象类型**: +- **ValidateCredit**: 征信验证结果翻译 +- **ValidateIDCard**: 身份证验证结果翻译 + +**翻译字段**: +- **Value**: 英文翻译 +- **ValueSC**: 简体中文翻译 +- **ValueTC**: 繁体中文翻译 +- **MatchRules**: 匹配规则列表 + +### 6. CertificateContentMatchRules 配置 +**使用位置**: `KYCService.cs` +**作用**: 根据证件内容自动识别证件类型 + +```csharp +// 获取证件内容匹配规则 +var matchRules = _configuration.GetSection("AppConfig:KYCConfig:CertificateContentMatchRules") + .Get>(); + +// 自动识别证件类型 +private string DetermineCertificateTypeCode(string content) +``` + +**用途**: 通过分析 OCR 识别的文本内容,自动判断证件类型 + +### 7. CountryCodeMatchRules 配置 +**使用位置**: `KYCService.cs` +**作用**: 根据证件内容自动识别国家代码 + +```csharp +// 获取国家代码匹配规则 +var matchRules = _configuration.GetSection("AppConfig:KYCConfig:CountryCodeMatchRules") + .Get>(); + +// 自动识别国家代码 +private string findCountryCode(string content) +private string DetermineCountryCode(string content, string certificateCode) +``` + +**用途**: 通过分析证件内容,自动识别证件所属国家 + +### 8. AttachmentFileSetting 配置 +**使用位置**: `KYCService.cs` +**作用**: 定义附件文件类型设置和证件处理器配置 + +```csharp +// 获取附件文件设置 +var configValue = _configuration.GetSection("AppConfig:KYCConfig:AttachmentFileSetting") + .Get(); + +// 相关方法 +public async Task GetAttachmentFileSettingAsync() +public async Task UpdateAttachmentFileSettingAsync(AttachmentFileSetting input) +``` + +**配置内容**: +- 个人和企业的文件类型定义 +- 每种文件类型的处理器配置 +- 文件验证和更新策略 + +### 9. ScanAndValidateSetting 配置 +**使用位置**: `KYCService.cs` +**作用**: 定义扫描和验证模式设置 + +```csharp +// 获取扫描验证设置 +var configSection = _configuration.GetSection("AppConfig:KYCConfig:ScanAndValidateSetting"); + +// 相关方法 +public async Task GetScanAndValidateSettingAsync() +public async Task UpdateScanAndValidateSettingAsync(ScanAndValidateSetting input) +``` + +**配置模式**: +- **ScanMode**: 扫描模式(OCR、芯片扫描等) +- **InputMode**: 输入模式(手动输入等) + +### 10. ScanAndValidateHandlers 配置 +**使用位置**: `HandlerService.cs` +**作用**: 定义所有可用的扫描和验证处理器 + +```csharp +// 获取处理器配置 +var handlersConfig = _configuration.GetSection("AppConfig:KYCConfig:ScanAndValidateHandlers") + .Get>(); + +// 处理器执行 +public async Task Handle(HandleInputDto input) +``` + +**处理器类型**: +- **OCR 处理器**: OCRIDCard, OCRBusinessLicense, OCRDocument, OCRTable +- **验证处理器**: ValidateIDCard, ValidatePassport, ValidateCredit, ValidateBR +- **扫描处理器**: Scan_OCR, Scan_Chin + +### 11. CertificateMappings 配置 +**使用位置**: `KYCService.cs` +**作用**: 证件映射配置(用于扩展证件类型支持) + +```csharp +// 获取证件映射 +mappings = _configuration.GetSection("AppConfig:KYCConfig:CertificateMappings") + .Get>(); +``` + +**用途**: 预留的扩展配置,用于支持更多证件类型的映射关系 + +## 数据库存储 + +KYC 配置还涉及数据库存储: +- **AttachmentFileSettingValue**: 存储租户级别的附件文件设置 +- **ScanAndValidateSettingValue**: 存储租户级别的扫描验证设置 + +这些设置可以覆盖全局配置,实现租户级别的个性化配置。 + +## 总结 + +KYCConfig 配置是一个完整的 KYC 系统配置框架,通过以下方式实现功能: + +1. **服务集成**: 与祥云 OCR 服务深度集成,支持多种证件识别和验证 +2. **代码转换**: 通过映射表实现内部代码与外部服务代码的转换 +3. **字段解析**: 智能解析 OCR 结果,提取结构化数据 +4. **自动识别**: 基于内容规则自动识别证件类型和国家 +5. **多语言支持**: 提供完整的多语言翻译机制 +6. **灵活配置**: 支持租户级别的个性化配置 +7. **处理器架构**: 模块化的处理器设计,支持扩展 + +该配置系统为 AML 系统提供了强大的 KYC 功能支持,能够处理复杂的证件识别、验证和管理需求。 \ No newline at end of file diff --git a/docs/KYC/Scanner/DKePassport-release-1.3.0/说明文档.md b/docs/KYC/Scanner/DKePassport-release-1.3.0/说明文档.md new file mode 100644 index 0000000..a2ad3b1 --- /dev/null +++ b/docs/KYC/Scanner/DKePassport-release-1.3.0/说明文档.md @@ -0,0 +1,163 @@ +这是一份根据您提供的《多元证件识别终端Windows版接口手册V1.0》整理的完整Markdown文档。我已保留了所有技术细节、参数表格、错误代码及代码示例。 + +--- + +# 德科物联 多元识别智能终端(Windows版本)接口手册 + +[cite_start]**版本:** V1.0 [cite: 4, 8] +[cite_start]**文档修订历史:** 2025.10.12 创建文档 [cite: 13] + +--- + +## 1. 免责声明 +[cite_start]本文档提供有关深圳市德科物联技术有限公司产品的信息。本文档并未以暗示、禁止反言或其他形式转让本公司或任何第三方的专利、商标、版权或所有权或其下的任何权利或许可 [cite: 15][cite_start]。除德科物联在其产品的销售条款和条件中声明的责任之外,本公司概不承担任何其它责任 [cite: 15][cite_start]。若不按手册要求连接或操作产生的问题,本公司免责 [cite: 15][cite_start]。德科物联可能随时对产品规格及产品描述作出修改,恕不另行通知 [cite: 15]。 + +--- + +## 2. 产品简介 + +### 2.1 产品外观与尺寸 +* [cite_start]**正面尺寸:** 142mm (宽) × 165mm (深) [cite: 53]。 +* [cite_start]**高度:** 88mm [cite: 53]。 +* [cite_start]**接口包含:** 电源接口、开关、Type-C、USB、网口、HDMI [cite: 55]。 + +### 2.2 产品基本功能 +1. [cite_start]**核验功能:** 集成身份证、粤居码、护照、港澳居民来往内地通行证、台湾居民来往大陆通行证、港澳居民居住证、台湾居民居住证、外国人永久居留身份证等核验能力,采用芯片识读 + OCR(辅助) [cite: 58]。 +2. [cite_start]**扩展识别:** 集成移动网证(数字身份)、CTID、中移超级 SIM 卡识别能力(定制) [cite: 59][cite_start],及电子身份证核验能力(后期扩展) [cite: 60]。 +3. [cite_start]**信息安全:** 所有核验信息可按接入方定制加密 [cite: 63]。 +4. [cite_start]**对接能力:** 免驱,通过 USB 线与 PC 连接,支持 HDMI 接口接到显示器显示结果(定制) [cite: 65][cite_start]。配套 WebSocket 服务,提供标准化接口与第三方 PMS/自助机对接 [cite: 66]。 +5. [cite_start]**系统直连:** 直连广东省旅业治安管理系统、广东省网约房管理平台 [cite: 67]。 +6. [cite_start]**适用场景:** 酒店、民宿、酒店公寓、网约房、边防检查站、机场、涉外旅行社等 [cite: 69]。 + +### 2.3 基本参数表格 +| 类别 | 参数项 | 具体规格 | +| :--- | :--- | :--- | +| **识别支持** | 自动触发 | [cite_start]支持证件自动感应触发识读 [cite: 71] | +| | 自动分类 | [cite_start]系统自动区分证件种类 [cite: 71] | +| | 证件识别(OCR) | [cite_start]支持二代证、护照(ICAO9303标准)、签证等图像采集与信息识别 [cite: 71] | +| | 二维码识别 | [cite_start]支持 1D (Code128, Code39, EAN-13)、2D (PDF417, QR, DataMatrix),速度 < 1秒 [cite: 71] | +| **硬件参数** | 图像采集 | [cite_start]500万像素摄像头,支持 BMP、JPG、PNG 格式输出 [cite: 71] | +| | 光源配置 | [cite_start]内置自然光、红外光、紫外光 [cite: 71] | +| | 机身尺寸 | [cite_start]193×169×104mm(注:手册中此处规格与示意图1-1标注有差异) [cite: 71] | +| | 产品重量 | [cite_start]0.654kg [cite: 71] | +| | 通讯接口 | [cite_start]USB 3.0 Type-C(1.5米线缆);支持双USB输入 [cite: 71] | +| | 电源规格 | [cite_start]输入 AC 100~240V;输出 DC 5V 6A [cite: 71] | +| **软件与升级** | 图像保存 | [cite_start]支持采集并减弱照片中光斑 [cite: 71] | +| | 系统支持 | [cite_start]Windows® 2000-SP4/XP/Vista/7/8/10 以及 Linux® [cite: 71] | +| | 固件更新 | [cite_start]支持 OTA 升级 [cite: 71] | +| **工作环境** | 湿度/温度 | [cite_start]20%~95%(无凝结);-10ºC ~ 50ºC [cite: 71] | + +### 2.4 设备状态查询和显示 +| 灯色 | 含义 | 备注 | +| :--- | :--- | :--- | +| 绿灯 | 识别成功 | [cite_start]所有功能识别成功时亮绿灯 [cite: 73] | +| 红灯 | 识别失败 | [cite_start]身份证识别失败(中途停止、被拦截、无网络)时亮红灯 [cite: 73] | +| 蓝灯 | 电源状态 | [cite_start]指示设备电源情况 [cite: 73] | +| 黄灯 | 读卡中 | [cite_start]处于芯片读取(NFC)过程时亮黄灯 [cite: 73] | + +--- + +## 3. 接口说明 + +### 3.1 接口格式定义 +* [cite_start]**通信协议:** WebSocket,本地端口侦听 [cite: 76, 77]。 +* [cite_start]**访问 URL:** `ws://127.0.0.1:xxx` [cite: 77]。 +* [cite_start]**数据格式:** JSON,驼峰命名法,区分大小写 [cite: 79]。 + +### 3.2 指令格式 + +#### 3.2.1 请求字段 +| 名称 | 说明 | 取值 | 备注 | +| :--- | :--- | :--- | :--- | +| `*command` | 指令 | 'read', 'scan', 'get', 'set', 'readCode', 'getPhoto', 'scanRaw' | [cite_start]对应读证、扫描、获取、设置等操作 [cite: 82] | +| `operand` | 操作对象 | 例如 'deviceName' | [cite_start]当 command 为 'get' 或 'set' 时配合使用 [cite: 82] | +| `param` | 参数 | | [cite_start]执行指令所需的参数 [cite: 82] | + +#### 3.2.2 应答字段 +| 名称 | 说明 | 取值与含义 | +| :--- | :--- | :--- | +| `*code` | 编码 | 0:成功; 1:设备断开; 2:设备故障; 3:处理超时; 4:未识别到证件; 5:读卡失败; 6:识别失败; 7:初始化失败; 8:未初始化; 9:未知类型; 10:头像获取失败; 11:配置出错; 12:端口占用; 13:参数不合法; 14:无授权文件; 15:授权不通过; 16:不支持操作; 17:假证; 18:证件过期; 19:证件作废; 20:授权过期; 21:已读过; [cite_start]99:未知错误 [cite: 84] | +| `message` | 应答信息 | [cite_start]失败时返回的出错原因描述 [cite: 84] | +| `data` | 返回值 | [cite_start]成功时返回的数据 [cite: 84] | +| `*command` | 原指令名 | [cite_start]返回请求中的 command [cite: 84] | +| `operand` | 原操作对象 | [cite_start]返回请求中的 operand [cite: 84] | + +--- + +## 4. 指令详述 + +### 4.1 获取信息 (`command: 'get'`) +* [cite_start]**获取名称:** `operand: 'name'` -> 返回设备名称 [cite: 87]。 +* [cite_start]**获取型号:** `operand: 'model'` -> 返回设备型号 [cite: 89]。 +* [cite_start]**获取序列号:** `operand: 'serialNo'` -> 返回序列号 [cite: 91]。 +* **获取设备类型:** `operand: 'type'` -> '01':读卡; '02':扫描; '03':一体式; [cite_start]'04':扫描(无OCR) [cite: 93]。 + +### 4.2 设置参数 (`command: 'set'`) +* [cite_start]**设置连续读取:** `operand: 'auto'`, `param: 0`(不自动) 或 `1`(自动) [cite: 96]。 + +### 4.3 证件信息字段说明 (`data` 对象) +| 名称 | 说明 | 取值/备注 | +| :--- | :--- | :--- | +| `*guestType` | 旅客类型 | '100':国内; '200':港澳台; [cite_start]'300':国外 [cite: 99] | +| `*name` | [cite_start]姓名 | [cite: 99] | +| `*sex` | 性别 | '1':男; [cite_start]'2':女 [cite: 99] | +| `*birthday` | [cite_start]出生日期 | yyyy-MM-dd [cite: 99] | +| `*cardType` | 证件类型 | 11:身份证; 12:居住证; 14:国外护照; 16:台胞证; 34:外国人永居证; 55:港澳台居住证; 60:回乡证; [cite_start]93:国内护照等 [cite: 99] | +| `*cardNo` | [cite_start]证件号码 | [cite: 99] | +| `*curPhoto` | [cite_start]证件头像 | jpg 转 Base64 编码 [cite: 99] | +| `photo` | [cite_start]现场头像 | jpg 转 Base64 编码 [cite: 99] | +| `nation` | 民族 | [cite_start]代码 '01'~'56', '59', '98', '57' (如 '01':汉, '02':蒙古) [cite: 99] | +| `adminDivision`| 行政区划 | [cite_start]行政区划代码 [cite: 99] | +| `firstName` | 英文姓 | [cite_start]国外旅客必填 [cite: 99] | +| `lastName` | 英文名 | [cite_start]国外旅客必填 [cite: 99] | +| `*nationalityArea`| 国籍/地区 | [cite_start]国籍或地区代码 [cite: 99] | +| `address` | 住址 | [cite_start]国内旅客必填 [cite: 99] | +| `signDate` | [cite_start]签证日期 | [cite: 99] | +| `validDate` | [cite_start]有效期结束 | [cite: 99] | +| `signOrg` | [cite_start]签发机关 | [cite: 99] | + +--- + +## 5. 接口调用示例 + +### 5.1 建立连接 (JS) +```javascript +try { + var websocket = new WebSocket('ws://127.0.0.1:xxx'); [cite_start]// http 方式 [cite: 112, 114] + websocket.onmessage = function(event) { + [cite_start]let data = JSON.parse(event.data); [cite: 121] + } +} catch (exception) { console.log('error.'); } +``` + +### 5.2 发送获取名称命令 +```javascript +[cite_start]let jsonData = { command: 'get', operand: 'name' }; [cite: 131] +[cite_start]websocket.send(JSON.stringify(jsonData)); [cite: 133] +``` + +### 5.3 返回读卡信息示例 +```json +{ + "code": 0, + "command": "read", + "data": { + "guestType": "100", + "name": "张三", + "sex": "1", + "birthday": "2000-01-01", + "cardType": "11", + "cardNo": "44xxx", + "nation": "01", + "address": "广东省珠海市香洲区 xxx", + "validDate": "2040-01-01" + } +[cite_start]} [cite: 138-162] +``` + +--- + +## 6. 技术支持 +* [cite_start]**公司:** 深圳市德科物联技术有限公司 [cite: 198] +* [cite_start]**网址:** [www.derkiot.com](https://www.derkiot.com) [cite: 199] +* [cite_start]**邮箱:** weiting@derkiot.com [cite: 200] \ No newline at end of file diff --git a/docs/KYC/Scanner/Scanner_Integration.md b/docs/KYC/Scanner/Scanner_Integration.md index bfda192..f8a4ad4 100644 --- a/docs/KYC/Scanner/Scanner_Integration.md +++ b/docs/KYC/Scanner/Scanner_Integration.md @@ -210,6 +210,102 @@ passportReader.onImageResult = (images) => { ... } ## 版本信息 -**文档版本**: v1.1 +**文档版本**: v1.2 **创建日期**: 2026-04-30 -**更新日期**: 2026-04-30 \ No newline at end of file +**更新日期**: 2026-04-30 + +--- + +## 后端配置映射说明 + +### 扫描仪数据流向 + +``` +扫描仪硬件 (返回特定格式的数据) + ↓ +CertificateMappings/*.json (定义 ScanerMainCertificateCode 等硬件指令码) + ↓ +appsettings.json - KYCConfig (定义 XiangyunAPI OCR映射 和 CertificateEntityColumParseRules 字段解析规则) + ↓ +后端 KYCService 处理 +``` + +### 关键配置文件 + +#### 1. CertificateMappings/*.json + +定义每种证件类型的字段映射: + +| 字段 | 说明 | +|------|------| +| `ScanerMainCertificateCode` | 扫描仪主证件类型代码 (如 "2"=身份证正面) | +| `ScanerBackCertificateCode` | 扫描仪背面证件类型代码 | +| `OCRCode` / `OCRCodeBack` | 祥云OCR API的证件类型代码 | +| `Columns[].VIZ` | 视读区字段名 | +| `Columns[].MRZ` | 机读码字段名 | +| `Columns[].OCR` | OCR识别字段名 | +| `Columns[].RFID` | 芯片读取字段名 | + +#### 2. appsettings.json - KYCConfig + +**CertificateEntityColumParseRules** - 字段解析规则: + +```json +{ + "AMLCode": "*", + "EntityColumn": "Name", + "EntityColumnNames": [ "姓名", "本国姓名", "名称", "Name", "name" ] +} +``` + +将扫描仪返回的原始字段名映射到系统标准字段名。 + +**XiangyunAPI.CertificateCodeMappings** - OCR映射: + +```json +{ + "AMLCode": "IDCard_CHN", + "OCRCode": "2", + "Remark": "二代身份证正面" +} +``` + +### 当前扫描仪字段来源 + +| 字段来源 | 说明 | +|---------|------| +| `VIZ` | 视读区 (Visual Inspection Zone) | +| `MRZ` | 机读码 (Machine Readable Zone) | +| `OCR` | OCR识别结果 (通过祥云API) | +| `RFID` | 芯片读取数据 | + +### 新SDK (DKePassport) 返回的字段名 + +根据演示页面,DKePassport返回字段包括: +- `name`, `sex`, `birthday`, `cardType`, `cardNo`, `nation`, `nationalityArea` +- `curPhoto`, `photo` (Base64图片) +- `guestType`, `adminDivision`, `address` 等 + +### 换扫描仪后的配置修改 + +| 配置项 | 当前位置 | 需要修改的内容 | +|--------|----------|---------------| +| 扫描仪指令码 | `CertificateMappings/*.json` → `ScanerMainCertificateCode` | **这套配置是给当前扫描仪用的,新扫描仪可能不需要或需要重新映射** | +| 字段解析规则 | `appsettings.json` → `CertificateEntityColumParseRules` | `EntityColumnNames` 需要对应新SDK返回的字段名 | +| OCR映射 | `appsettings.json` → `XiangyunAPI.CertificateCodeMappings` | AMLCode/OCRCode 可能需要调整 | + +### 证件类型代码对比 + +| 证件类型 | 当前SDK (Sinosecu) | 新SDK (DKePassport) | +|---------|-------------------|---------------------| +| 身份证正面 | `2` | `11` | +| 身份证背面 | `3` | - | +| 护照 | `13` | `14` (国外护照) | +| 港澳居民来往内地通行证-照片页 | `14` | `60` | +| 港澳居民来往内地通行证-机读码页 | `15` | - | + +### 注意事项 + +1. **DKePassport的 `cardType` 代码体系**与当前的 `ScanerMainCertificateCode` 不同 +2. 新SDK返回的字段名(如 `name`, `cardNo`)与当前系统期望的字段名(如 `Name`, `Number`)需要通过 `CertificateEntityColumParseRules` 映射 +3. 如果DKePassport不提供MRZ/RFID区分,可能需要调整 `Columns` 中的字段映射策略 \ No newline at end of file