主表字段 测试时间(年、月、日),测试时间(时、分、秒),测试结果(固定为三种:0:0:1;0:测试为OK,0;0;0:1:测试为Fail,T面(分Top(上)面/Bottom(下)面),测试点数,实际不良,设备型号,批次工单,机种名,条码,SMT组别,线别 子表字段 偏位,缺件,反向,翘起,浮高,立碑,翻转,错件,翘脚,虚焊,空焊,少锡,多锡,连锡,漏铜,拉尖,异物,溢胶,引脚偏位,生产线名称,机器名称,投入板数,OK板数,通过板数,合格率,不良板数,不良率,不良率(PPM),不良点数,实测点数,待测点数 执行目标 - 将 `MES.Service/service/QC/SPIAOI.txt`:1-7 中的 SPI/AOI 检测字段转化为一个可调用的 REST API,并补充数据库建表与持久化逻辑。 - 目标覆盖请求契约、数据校验、主从表建模、存储过程/服务保存流程以及异常与日志处理。 数据结构拆解 - `主表字段` → 记录单次 SPI/AOI 检测的头信息。建议字段/类型: 测试日期(`TestDate`/DATE)、测试时间(`TestTime`/VARCHAR2(8 CHAR))、测试结果(`TestResult`/VARCHAR2(12 CHAR),值域:`0:0:1;0`、`0;0;0:1`、`Fail`)、面别(`Surface`/CHAR(1),值域 `T`/`B`)、测试点数(`TotalPoints`/NUMBER(6))、实际不良点数(`ActualDefects`/NUMBER(6))、设备型号(`EquipmentModel`/VARCHAR2(64 CHAR))、批次工单(`WorkOrder`/VARCHAR2(64 CHAR))、机种名(`ProductModel`/VARCHAR2(64 CHAR))、条码(`BoardBarcode`/VARCHAR2(128 CHAR))、SMT 组别(`SmtGroup`/VARCHAR2(32 CHAR))、线别(`LineName`/VARCHAR2(32 CHAR))。 - `子表字段` → 记录单次检测的缺陷统计。建议字段/类型: 偏位(`OffsetCount`/NUMBER(6))、缺件(`MissingCount`/NUMBER(6))、反向(`ReverseCount`/NUMBER(6))、翘起(`LiftedCount`/NUMBER(6))、浮高(`FloatHighCount`/NUMBER(6))、立碑(`TombstoneCount`/NUMBER(6))、翻转(`FlipCount`/NUMBER(6))、错件(`WrongPartCount`/NUMBER(6))、翘脚(`LeadLiftCount`/NUMBER(6))、虚焊(`ColdJointCount`/NUMBER(6))、空焊(`NoSolderCount`/NUMBER(6))、少锡(`InsufficientSolderCount`/NUMBER(6))、多锡(`ExcessSolderCount`/NUMBER(6))、连锡(`BridgeCount`/NUMBER(6))、漏铜(`CopperExposureCount`/NUMBER(6))、拉尖(`SpikeCount`/NUMBER(6))、异物(`ForeignMatterCount`/NUMBER(6))、溢胶(`GlueOverflowCount`/NUMBER(6))、引脚偏位(`PinOffsetCount`/NUMBER(6))、生产线名称(`LineDisplayName`/VARCHAR2(64 CHAR))、机器名称(`MachineName`/VARCHAR2(64 CHAR))、投入板数(`InputBoards`/NUMBER(6))、OK板数(`OkBoards`/NUMBER(6))、通过板数(`PassBoards`/NUMBER(6))、合格率(`PassRate`/NUMBER(5,2))、不良板数(`DefectBoards`/NUMBER(6))、不良率(`DefectRate`/NUMBER(5,2))、不良率PPM(`DefectPpm`/NUMBER(9))、不良点数(`DefectPoints`/NUMBER(6))、实测点数(`MeasuredPoints`/NUMBER(6))、待测点数(`PendingPoints`/NUMBER(6))。所有“率”字段以百分比存储,入库前转为数值型。 数据库设计 - 目标库为 Oracle 11g(11.2),本项目使用 `SqlSugar` 并通过 `[SugarColumn(OracleSequenceName = "...")]` 自动取号,因此只需声明主键和序列,无需触发器。示例 DDL: ``` -- 主表:记录每块电路板的 SPI/AOI 批次检测摘要数据 -- SPI/AOI 主表,记录单板批次检测摘要 CREATE TABLE MES_SPI_AOI_HEADER ( ID NUMBER(19) NOT NULL, TEST_DATE DATE NOT NULL, TEST_TIME VARCHAR2(8 CHAR) NOT NULL, TEST_RESULT VARCHAR2(12 CHAR) NOT NULL, SURFACE CHAR(1) NOT NULL CHECK (SURFACE IN ('T','B')), TOTAL_POINTS NUMBER(6), ACTUAL_DEFECTS NUMBER(6), EQUIPMENT_MODEL VARCHAR2(64 CHAR), WORK_ORDER VARCHAR2(64 CHAR), PRODUCT_MODEL VARCHAR2(64 CHAR), BOARD_BARCODE VARCHAR2(128 CHAR) NOT NULL, SMT_GROUP VARCHAR2(32 CHAR), LINE_NAME VARCHAR2(32 CHAR), CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP, UPDATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP ); ALTER TABLE MES_SPI_AOI_HEADER ADD CONSTRAINT PK_SPI_AOI_HEADER PRIMARY KEY (ID); CREATE UNIQUE INDEX IDX_SPI_AOI_BARCODE ON MES_SPI_AOI_HEADER (BOARD_BARCODE); COMMENT ON TABLE MES_SPI_AOI_HEADER IS 'SPI/AOI 主表,记录单板检测摘要数据'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.ID IS '主键ID,由序列 SEQ_SPI_AOI_HEADER 生成'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.TEST_DATE IS '测试日期,沿用设备上传的年月日'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.TEST_TIME IS '测试时间,沿用设备上传的 HH:mm:ss'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.TEST_RESULT IS '测试结果原始字符串,例如 0:0:1;0、0;0;0:1、Fail'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.SURFACE IS '检测面,T=Top 顶面,B=Bottom 底面'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.TOTAL_POINTS IS '计划检测点数'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.ACTUAL_DEFECTS IS '实际不良点数'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.EQUIPMENT_MODEL IS '设备型号'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.WORK_ORDER IS '对应工单或批次号'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.PRODUCT_MODEL IS '机种名称'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.BOARD_BARCODE IS '板件条码,整单唯一'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.SMT_GROUP IS 'SMT 组别'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.LINE_NAME IS '线别名称'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.CREATED_AT IS '创建时间,默认写入数据库时间戳'; COMMENT ON COLUMN MES_SPI_AOI_HEADER.UPDATED_AT IS '更新时间,默认写入数据库时间戳'; CREATE SEQUENCE SEQ_SPI_AOI_HEADER START WITH 1 INCREMENT BY 1 NOCACHE; -- SPI/AOI 明细表,拆分缺陷统计与产能指标 CREATE TABLE MES_SPI_AOI_DETAIL ( ID NUMBER(19) NOT NULL, HEADER_ID NUMBER(19) NOT NULL, OFFSET_COUNT NUMBER(6) DEFAULT 0, MISSING_COUNT NUMBER(6) DEFAULT 0, REVERSE_COUNT NUMBER(6) DEFAULT 0, LIFTED_COUNT NUMBER(6) DEFAULT 0, FLOAT_HIGH_COUNT NUMBER(6) DEFAULT 0, TOMBSTONE_COUNT NUMBER(6) DEFAULT 0, FLIP_COUNT NUMBER(6) DEFAULT 0, WRONG_PART_COUNT NUMBER(6) DEFAULT 0, LEAD_LIFT_COUNT NUMBER(6) DEFAULT 0, COLD_JOINT_COUNT NUMBER(6) DEFAULT 0, NO_SOLDER_COUNT NUMBER(6) DEFAULT 0, INSUFFICIENT_SOLDER_COUNT NUMBER(6) DEFAULT 0, EXCESS_SOLDER_COUNT NUMBER(6) DEFAULT 0, BRIDGE_COUNT NUMBER(6) DEFAULT 0, COPPER_EXPOSURE_COUNT NUMBER(6) DEFAULT 0, SPIKE_COUNT NUMBER(6) DEFAULT 0, FOREIGN_MATTER_COUNT NUMBER(6) DEFAULT 0, GLUE_OVERFLOW_COUNT NUMBER(6) DEFAULT 0, PIN_OFFSET_COUNT NUMBER(6) DEFAULT 0, LINE_DISPLAY_NAME VARCHAR2(64 CHAR), MACHINE_NAME VARCHAR2(64 CHAR), INPUT_BOARDS NUMBER(6) DEFAULT 0, OK_BOARDS NUMBER(6) DEFAULT 0, PASS_BOARDS NUMBER(6) DEFAULT 0, PASS_RATE NUMBER(5,2), DEFECT_BOARDS NUMBER(6) DEFAULT 0, DEFECT_RATE NUMBER(5,2), DEFECT_PPM NUMBER(9), DEFECT_POINTS NUMBER(6) DEFAULT 0, MEASURED_POINTS NUMBER(6) DEFAULT 0, PENDING_POINTS NUMBER(6) DEFAULT 0, CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP, UPDATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP ); ALTER TABLE MES_SPI_AOI_DETAIL ADD CONSTRAINT PK_SPI_AOI_DETAIL PRIMARY KEY (ID); ALTER TABLE MES_SPI_AOI_DETAIL ADD CONSTRAINT FK_SPI_AOI_DETAIL_HEADER FOREIGN KEY (HEADER_ID) REFERENCES MES_SPI_AOI_HEADER(ID); CREATE INDEX IDX_SPI_AOI_DETAIL_HEADER ON MES_SPI_AOI_DETAIL (HEADER_ID); COMMENT ON TABLE MES_SPI_AOI_DETAIL IS 'SPI/AOI 明细表,记录缺陷分类统计与产线效率数据'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.ID IS '主键ID,由序列 SEQ_SPI_AOI_DETAIL 生成'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.HEADER_ID IS '主表外键,指向 MES_SPI_AOI_HEADER.ID'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.OFFSET_COUNT IS '偏位数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.MISSING_COUNT IS '缺件数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.REVERSE_COUNT IS '反向安装数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.LIFTED_COUNT IS '翘起数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.FLOAT_HIGH_COUNT IS '浮高数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.TOMBSTONE_COUNT IS '立碑数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.FLIP_COUNT IS '翻转数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.WRONG_PART_COUNT IS '错件数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.LEAD_LIFT_COUNT IS '翘脚数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.COLD_JOINT_COUNT IS '虚焊数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.NO_SOLDER_COUNT IS '空焊数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.INSUFFICIENT_SOLDER_COUNT IS '少锡数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.EXCESS_SOLDER_COUNT IS '多锡数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.BRIDGE_COUNT IS '连锡数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.COPPER_EXPOSURE_COUNT IS '漏铜数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.SPIKE_COUNT IS '拉尖数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.FOREIGN_MATTER_COUNT IS '异物残留数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.GLUE_OVERFLOW_COUNT IS '溢胶数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.PIN_OFFSET_COUNT IS '引脚偏位数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.LINE_DISPLAY_NAME IS '产线显示名称'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.MACHINE_NAME IS '检测机台名称'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.INPUT_BOARDS IS '投入板数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.OK_BOARDS IS 'OK 板数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.PASS_BOARDS IS '通过板数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.PASS_RATE IS '合格率(%)'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.DEFECT_BOARDS IS '不良板数量'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.DEFECT_RATE IS '不良率(%)'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.DEFECT_PPM IS '不良数(PPM)'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.DEFECT_POINTS IS '不良点数'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.MEASURED_POINTS IS '实测点数'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.PENDING_POINTS IS '待测点数'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.CREATED_AT IS '创建时间,默认写入数据库时间戳'; COMMENT ON COLUMN MES_SPI_AOI_DETAIL.UPDATED_AT IS '更新时间,默认写入数据库时间戳'; CREATE SEQUENCE SEQ_SPI_AOI_DETAIL START WITH 1 INCREMENT BY 1 NOCACHE; 测试结果说明 - `TestResult` 字段直接接收原始字符串,不做枚举校验 - 常见值参考: - "0:0:1;0" (通常表示测试通过) - "0;0;0:1" (通常表示测试失败) - "Fail" (通常表示测试异常) - 数据库存储原始值,由业务层根据需要解析 接口设计 - URL:`POST /api/QC/SpiAoi/Upload`(位于 `MESApplication.Controllers.QC` 命名空间,沿用 `ResponseResult` 返回格式)。 - 请求体结构: ``` { "header": { "testDate": "2025-10-10", "testTime": "14:33:21", "testResult": "0;0;0:1", "surface": "T", "totalPoints": 500, "actualDefects": 3, "equipmentModel": "SPI-9000", "workOrder": "WO20251010-01", "productModel": "MODEL-ABC", "boardBarcode": "BC123456789", "smtGroup": "A1", "lineName": "SMT-01" }, "details": [ { "machineName": "AOI-01", "lineDisplayName": "SMT-01", "offsetCount": 1, "missingCount": 0, "reverseCount": 0, "liftedCount": 0, "floatHighCount": 0, "tombstoneCount": 0, "flipCount": 0, "wrongPartCount": 0, "leadLiftCount": 0, "coldJointCount": 0, "noSolderCount": 0, "insufficientSolderCount": 1, "excessSolderCount": 0, "bridgeCount": 1, "copperExposureCount": 0, "spikeCount": 0, "foreignMatterCount": 0, "glueOverflowCount": 0, "pinOffsetCount": 0, "inputBoards": 120, "okBoards": 117, "passBoards": 117, "passRate": 97.5, "defectBoards": 3, "defectRate": 2.5, "defectPpm": 25000, "defectPoints": 3, "measuredPoints": 500, "pendingPoints": 0 } ] } ``` - 响应示例: ``` { "status": 0, "message": "OK", "data": { "headerId": 12345 } } ``` 业务处理流程 - 入口控制器负责模型绑定与基本校验: - 必填字段:testDate、testTime、testResult、boardBarcode、surface - 枚举合法性:surface 必须为 "T" 或 "B" - testResult:不做枚举校验,直接接收原始字符串 - 数值非负:所有计数字段必须 >= 0 - **details 不能为空或 null** - 将请求委托给 `SpiAoiService`(位于 `MES.Service.service.QC`),该服务使用 `SqlSugarClient` 启动事务: 1. **条码唯一性检测**:若 `BOARD_BARCODE` 已存在,直接返回错误 `status=1, message="条码已存在,不允许重复上传"`(保持唯一索引约束)。 2. 插入头表记录,返回 `HEADER_ID`(实体通过 `[SugarColumn(IsPrimaryKey = true, OracleSequenceName = "SEQ_SPI_AOI_HEADER")]` 自动获取序列值)。 3. 遍历 `details`,为每条填充 `HEADER_ID` 并插入子表。 4. **衍生字段校验**(记录日志但不阻断): - `passBoards <= inputBoards` - `defectBoards = inputBoards - passBoards` - `passRate` 与计算值 `(passBoards / inputBoards * 100)` 的偏差在 ±1.0 以内 - 若校验失败,记录警告日志但不回滚事务 5. 提交事务;异常则回滚并记录日志。 - 成功后返回 `headerId`;失败返回 `status=1` 与错误信息。 验证与错误处理 - **输入校验失败**:返回 `status=1`,`message` 指明字段及原因(如:"boardBarcode 不能为空"、"surface 必须为 T 或 B")。 - **条码重复**:返回 `status=1, message="条码 {boardBarcode} 已存在,不允许重复上传"`。 - **details 为空**:返回 `status=1, message="details 不能为空"`。 - **数据库异常**:捕获 `SqlSugar`/Oracle 异常,记录到现有日志(如 `LogUtil`),同时写入消息中心(若需 ERP 同步)或自定义错误队列。 实体定义示例 ```csharp [SugarTable("MES_SPI_AOI_HEADER")] public class SpiAoiHeader { [SugarColumn(ColumnName = "ID", IsPrimaryKey = true, OracleSequenceName = "SEQ_SPI_AOI_HEADER")] public long Id { get; set; } [SugarColumn(ColumnName = "TEST_DATE")] public DateTime TestDate { get; set; } [SugarColumn(ColumnName = "TEST_TIME")] public string TestTime { get; set; } [SugarColumn(ColumnName = "TEST_RESULT")] public string TestResult { get; set; } [SugarColumn(ColumnName = "SURFACE")] public string Surface { get; set; } [SugarColumn(ColumnName = "TOTAL_POINTS")] public int? TotalPoints { get; set; } [SugarColumn(ColumnName = "ACTUAL_DEFECTS")] public int? ActualDefects { get; set; } [SugarColumn(ColumnName = "EQUIPMENT_MODEL")] public string EquipmentModel { get; set; } [SugarColumn(ColumnName = "WORK_ORDER")] public string WorkOrder { get; set; } [SugarColumn(ColumnName = "PRODUCT_MODEL")] public string ProductModel { get; set; } [SugarColumn(ColumnName = "BOARD_BARCODE")] public string BoardBarcode { get; set; } [SugarColumn(ColumnName = "SMT_GROUP")] public string SmtGroup { get; set; } [SugarColumn(ColumnName = "LINE_NAME")] public string LineName { get; set; } [SugarColumn(ColumnName = "CREATED_AT")] public DateTime CreatedAt { get; set; } [SugarColumn(ColumnName = "UPDATED_AT")] public DateTime UpdatedAt { get; set; } } [SugarTable("MES_SPI_AOI_DETAIL")] public class SpiAoiDetail { [SugarColumn(ColumnName = "ID", IsPrimaryKey = true, OracleSequenceName = "SEQ_SPI_AOI_DETAIL")] public long Id { get; set; } [SugarColumn(ColumnName = "HEADER_ID")] public long HeaderId { get; set; } [SugarColumn(ColumnName = "OFFSET_COUNT")] public int OffsetCount { get; set; } [SugarColumn(ColumnName = "MISSING_COUNT")] public int MissingCount { get; set; } [SugarColumn(ColumnName = "REVERSE_COUNT")] public int ReverseCount { get; set; } [SugarColumn(ColumnName = "LIFTED_COUNT")] public int LiftedCount { get; set; } [SugarColumn(ColumnName = "FLOAT_HIGH_COUNT")] public int FloatHighCount { get; set; } [SugarColumn(ColumnName = "TOMBSTONE_COUNT")] public int TombstoneCount { get; set; } [SugarColumn(ColumnName = "FLIP_COUNT")] public int FlipCount { get; set; } [SugarColumn(ColumnName = "WRONG_PART_COUNT")] public int WrongPartCount { get; set; } [SugarColumn(ColumnName = "LEAD_LIFT_COUNT")] public int LeadLiftCount { get; set; } [SugarColumn(ColumnName = "COLD_JOINT_COUNT")] public int ColdJointCount { get; set; } [SugarColumn(ColumnName = "NO_SOLDER_COUNT")] public int NoSolderCount { get; set; } [SugarColumn(ColumnName = "INSUFFICIENT_SOLDER_COUNT")] public int InsufficientSolderCount { get; set; } [SugarColumn(ColumnName = "EXCESS_SOLDER_COUNT")] public int ExcessSolderCount { get; set; } [SugarColumn(ColumnName = "BRIDGE_COUNT")] public int BridgeCount { get; set; } [SugarColumn(ColumnName = "COPPER_EXPOSURE_COUNT")] public int CopperExposureCount { get; set; } [SugarColumn(ColumnName = "SPIKE_COUNT")] public int SpikeCount { get; set; } [SugarColumn(ColumnName = "FOREIGN_MATTER_COUNT")] public int ForeignMatterCount { get; set; } [SugarColumn(ColumnName = "GLUE_OVERFLOW_COUNT")] public int GlueOverflowCount { get; set; } [SugarColumn(ColumnName = "PIN_OFFSET_COUNT")] public int PinOffsetCount { get; set; } [SugarColumn(ColumnName = "LINE_DISPLAY_NAME")] public string LineDisplayName { get; set; } [SugarColumn(ColumnName = "MACHINE_NAME")] public string MachineName { get; set; } [SugarColumn(ColumnName = "INPUT_BOARDS")] public int InputBoards { get; set; } [SugarColumn(ColumnName = "OK_BOARDS")] public int OkBoards { get; set; } [SugarColumn(ColumnName = "PASS_BOARDS")] public int PassBoards { get; set; } [SugarColumn(ColumnName = "PASS_RATE")] public decimal? PassRate { get; set; } [SugarColumn(ColumnName = "DEFECT_BOARDS")] public int DefectBoards { get; set; } [SugarColumn(ColumnName = "DEFECT_RATE")] public decimal? DefectRate { get; set; } [SugarColumn(ColumnName = "DEFECT_PPM")] public int? DefectPpm { get; set; } [SugarColumn(ColumnName = "DEFECT_POINTS")] public int DefectPoints { get; set; } [SugarColumn(ColumnName = "MEASURED_POINTS")] public int MeasuredPoints { get; set; } [SugarColumn(ColumnName = "PENDING_POINTS")] public int PendingPoints { get; set; } [SugarColumn(ColumnName = "CREATED_AT")] public DateTime CreatedAt { get; set; } [SugarColumn(ColumnName = "UPDATED_AT")] public DateTime UpdatedAt { get; set; } } ``` 实现步骤建议 1. **创建数据库表**:执行上述 DDL,创建 `MES_SPI_AOI_HEADER`、`MES_SPI_AOI_DETAIL` 表及序列。 2. **定义实体模型**:在 `MES.Service.Modes` 下创建 `SpiAoiHeader.cs` 和 `SpiAoiDetail.cs`(参考上述实体定义示例)。 3. **创建 DTO**: - `SpiAoiHeaderDto.cs`(用于请求体 header 部分) - `SpiAoiDetailDto.cs`(用于请求体 details 数组元素) - `SpiAoiUploadRequest.cs`(包含 Header 和 Details 列表) - `SpiAoiUploadResponse.cs`(包含 headerId) 4. **实现服务层**: - 在 `MES.Service.service.QC` 下创建 `SpiAoiService.cs` - 实现条码唯一性检查、事务插入、数据校验等业务逻辑 5. **实现控制器**: - 在 `MESApplication.Controllers.QC` 下创建 `SpiAoiController.cs` - 添加 `[HttpPost("Upload")]` 方法,调用服务层并返回 `ResponseResult` 6. **添加单元测试**(可选):验证成功插入、非法枚举、条码重复、details 为空、事务回滚等场景。 注意事项 - 所有字符串字段需检查长度是否超过数据库定义(如 `BOARD_BARCODE` 最大 128 字符)。 - `TestDate` 和 `TestTime` 分开存储,前端需分别传入日期和时间字符串。 - `TestResult` 不做枚举校验,接收任意字符串值(最大 12 字符),原样存储到数据库。 - 对于 `CREATED_AT` 和 `UPDATED_AT`,建议在代码中显式赋值 `DateTime.Now`,而非依赖数据库默认值。 - Oracle 11g 需确保 SqlSugar 版本兼容(推荐 5.x 或更高版本)。 - 条码唯一索引 `IDX_SPI_AOI_BARCODE` 会自动阻止重复插入,但建议在代码中提前检查并返回友好错误信息。