diff --git a/docs/backend-api.md b/docs/backend-api.md new file mode 100644 index 0000000..07b5f28 --- /dev/null +++ b/docs/backend-api.md @@ -0,0 +1,303 @@ +# Corewing QC 后端接口文档 + +> 基础路径: `/api/qc` + +## 通用响应结构 + +所有接口统一返回 `Result`: + +```json +{ + "code": 200, + "message": "操作成功", + "data": T, + "success": true +} +``` + +| 字段 | 类型 | 说明 | +|---------|---------|---------------------------| +| code | Integer | 状态码 | +| message | String | 消息内容 | +| data | T | 业务数据 | +| success | Boolean | 是否成功 | + +--- + +## 1. 验证无线板 + +验证 BLE MAC 地址对应的无线板是否已在系统中注册。 + +- **URL**: `POST /api/qc/wireless/validate-wireless` +- **Content-Type**: `application/json` + +### 请求体 + +```json +{ + "mac": "AA:BB:CC:DD:EE:FF" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|--------|------|----------------------------------| +| mac | String | 是 | BLE MAC 地址,即设备 SN | + +### 响应 + +```json +{ + "code": 200, + "message": "验证通过", + "data": true, + "success": true +} +``` + +- `data = true` — 设备已注册,允许进行 QC 测试 +- `data = false` / `success = false` — 设备未注册或验证失败,APP 端将**作废本次测试** + +--- + +## 2. 上传测试记录 + +上传完整的 QC 测试结果(含所有步骤数据和照片 URL)。 + +- **URL**: `POST /api/qc/test/upload` +- **Content-Type**: `application/json` + +### 请求体 + +```json +{ + "testNo": "WL-20260227-001", + "productType": "WIRELESS_BOARD", + "sn": "AA:BB:CC:DD:EE:FF", + "operatorId": "OP001", + "operatorName": "张三", + "phoneModel": "Pixel 7", + "appVersion": "1.0.0", + "status": "PASS", + "createTime": "1740000000000", + "updatedAt": "1740001000000", + "uploadTime": "1740002000000", + "steps": [ + { + "stepIndex": 1, + "stepName": "BLE连接与特征订阅", + "result": "PASS", + "dataJson": "{\"deviceName\":\"CoreWing-001\",\"deviceMac\":\"AA:BB:CC:DD:EE:FF\",\"validated\":true}", + "duration": 5200, + "completedAt": "1740000500000" + } + ], + "photoUrls": [ + "https://qc.corewing.com/photos/xxx.jpg" + ] +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|--------------|------------------|------|------------------------------------------------| +| testNo | String | 是 | 测试单号,格式: `WL-yyyyMMdd-NNN` 或 `FC-yyyyMMdd-NNN` | +| productType | String | 是 | `WIRELESS_BOARD` / `FLIGHT_CONTROLLER` | +| sn | String | 是 | 产品序列号(无线板为 BLE MAC 地址) | +| operatorId | String | 是 | 测试员 ID | +| operatorName | String | 是 | 测试员姓名 | +| phoneModel | String | 否 | 测试手机型号 | +| appVersion | String | 否 | APP 版本号 | +| status | String | 是 | `PASS` / `FAIL` | +| createTime | String | 是 | 创建时间(毫秒时间戳字符串) | +| updatedAt | String | 否 | 最后更新时间(毫秒时间戳字符串) | +| uploadTime | String | 否 | 上传时间(毫秒时间戳字符串) | +| steps | StepResultData[] | 是 | 步骤结果数组 | +| photoUrls | String[] | 否 | 已上传照片的远程 URL 列表 | + +**StepResultData:** + +| 字段 | 类型 | 必填 | 说明 | +|-------------|--------|------|--------------------------------------| +| stepIndex | int | 是 | 步骤序号(从 1 开始) | +| stepName | String | 是 | 步骤名称 | +| result | String | 否 | `PASS` / `FAIL` / null(未完成) | +| dataJson | String | 否 | 步骤详细数据 JSON 字符串 | +| duration | long | 否 | 步骤耗时(毫秒) | +| completedAt | String | 否 | 完成时间(毫秒时间戳字符串) | + +### 响应 + +```json +{ + "code": 200, + "message": "上传成功", + "data": "https://qc.corewing.com/report/WL-20260227-001", + "success": true +} +``` + +- `data` — 测试报告 URL(可为 null) + +--- + +## 3. 上传照片 + +上传测试步骤关联的照片。 + +- **URL**: `POST /api/qc/test/upload-photo` +- **Content-Type**: `multipart/form-data` + +### 请求参数 + +| 字段 | 类型 | 必填 | 说明 | +|--------|---------------|------|------------------| +| testNo | String (text) | 是 | 测试单号 | +| photo | File | 是 | 照片文件 (JPEG等)| + +### 响应 + +```json +{ + "code": 200, + "message": "上传成功", + "data": { + "url": "https://qc.corewing.com/photos/xxx.jpg" + }, + "success": true +} +``` + +--- + +## 4. 验证 SN + +验证序列号是否有效(飞控板扫码流程使用)。 + +- **URL**: `POST /api/qc/test/validate-sn` +- **Content-Type**: `application/json` + +### 请求体 + +```json +{ + "sn": "FC-SN-20260001" +} +``` + +### 响应 + +```json +{ + "code": 200, + "message": "SN 有效", + "data": true, + "success": true +} +``` + +--- + +## 5. 获取测试配置 + +获取指定产品类型的测试参数/阈值。 + +- **URL**: `GET /api/qc/config/test-params?productType=WIRELESS_BOARD` + +### 响应 + +```json +{ + "code": 200, + "success": true, + "data": { } +} +``` + +> `data` 结构由后端自定义,APP 当前未使用。 + +--- + +## 6. 查询 SN 历史记录 + +- **URL**: `GET /api/qc/test/history?sn=AA:BB:CC:DD:EE:FF` + +### 响应 + +```json +{ + "code": 200, + "success": true, + "data": { } +} +``` + +--- + +## 7. 获取测试报告 + +- **URL**: `GET /api/qc/test/report?testNo=WL-20260227-001` + +### 响应 + +```json +{ + "code": 200, + "success": true, + "data": "https://qc.corewing.com/report/WL-20260227-001" +} +``` + +--- + +## 枚举值参考 + +### productType(产品类型) + +| 值 | 说明 | +|---------------------|--------| +| WIRELESS_BOARD | 无线板 | +| FLIGHT_CONTROLLER | 飞控板 | + +### status(测试状态) + +| 值 | 说明 | +|-------------|----------| +| IN_PROGRESS | 进行中 | +| PASS | 通过 | +| FAIL | 失败 | +| UPLOADED | 已上传 | + +### result(步骤结果) + +| 值 | 说明 | +|------|------| +| PASS | 通过 | +| FAIL | 失败 | + +### 无线板测试步骤(7 步) + +| stepIndex | stepName | +|-----------|------------------------| +| 1 | BLE连接与特征订阅 | +| 2 | 安装无线板/外观检查 | +| 3 | 供电测试(4.5V) | +| 4 | Type-C USB测试 | +| 5 | BLE信号质量测试 | +| 6 | MAVLink心跳通信测试 | +| 7 | 激活/入库 | + +### 飞控板测试步骤(12 步) + +| stepIndex | stepName | +|-----------|-----------------| +| 1 | USB串口连接 | +| 2 | 安装SD卡与装夹 | +| 3 | 心跳检测 | +| 4 | IMU测试 | +| 5 | 气压计测试 | +| 6 | GPS模块检测 | +| 7 | RC输入测试 | +| 8 | ADC接口测试 | +| 9 | PWM输出测试 | +| 10 | 图传与OSD检查 | +| 11 | 参数重置 | +| 12 | 数据上传/结束 | diff --git a/docs/schema.sql b/docs/schema.sql new file mode 100644 index 0000000..a1dc9aa --- /dev/null +++ b/docs/schema.sql @@ -0,0 +1,81 @@ +-- ============================================================ +-- Corewing QC 后端数据库建表 SQL(MySQL) +-- ============================================================ + + +CREATE TABLE `flight_controller` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `sn` VARCHAR(64) NOT NULL COMMENT '序列号', + `model` VARCHAR(64) DEFAULT NULL COMMENT '型号', + `batch_no` VARCHAR(64) DEFAULT NULL COMMENT '批次号', + `status` VARCHAR(32) NOT NULL DEFAULT 'REGISTERED' COMMENT '状态: REGISTERED / QC_PASS / QC_FAIL / ACTIVATED', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_sn` (`sn`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='飞控板设备注册表'; + + +CREATE TABLE `qc_test_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `test_no` VARCHAR(32) NOT NULL COMMENT '测试单号,格式: WL-yyyyMMdd-NNN / FC-yyyyMMdd-NNN', + `product_type` VARCHAR(32) NOT NULL COMMENT '产品类型: WIRELESS_BOARD / FLIGHT_CONTROLLER', + `sn` VARCHAR(64) NOT NULL COMMENT '产品序列号(无线板为 BLE MAC)', + `operator_id` VARCHAR(32) NOT NULL COMMENT '测试员 ID', + `operator_name` VARCHAR(64) NOT NULL COMMENT '测试员姓名', + `phone_model` VARCHAR(64) DEFAULT NULL COMMENT '测试手机型号', + `app_version` VARCHAR(16) DEFAULT NULL COMMENT 'APP 版本', + `status` VARCHAR(16) NOT NULL COMMENT '测试结果: PASS / FAIL', + `create_time` DATETIME NOT NULL COMMENT 'APP 端创建时间', + `updated_at` DATETIME DEFAULT NULL COMMENT 'APP 端最后更新时间', + `upload_time` DATETIME DEFAULT NULL COMMENT 'APP 端上传时间', + `report_url` VARCHAR(512) DEFAULT NULL COMMENT '测试报告 URL', + `server_create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '服务端入库时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_test_no` (`test_no`), + KEY `idx_sn` (`sn`), + KEY `idx_product_type` (`product_type`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='QC 测试记录主表'; + + +CREATE TABLE `qc_step_result` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `test_record_id` BIGINT NOT NULL COMMENT '关联 qc_test_record.id', + `step_index` INT NOT NULL COMMENT '步骤序号(从 1 开始)', + `step_name` VARCHAR(64) NOT NULL COMMENT '步骤名称', + `result` VARCHAR(8) DEFAULT NULL COMMENT '步骤结果: PASS / FAIL / NULL', + `data_json` TEXT DEFAULT NULL COMMENT '步骤详细数据 JSON', + `duration` BIGINT DEFAULT 0 COMMENT '步骤耗时(毫秒)', + `completed_at` DATETIME DEFAULT NULL COMMENT '步骤完成时间', + PRIMARY KEY (`id`), + KEY `idx_test_record_id` (`test_record_id`), + UNIQUE KEY `uk_record_step` (`test_record_id`, `step_index`), + CONSTRAINT `fk_step_record` FOREIGN KEY (`test_record_id`) + REFERENCES `qc_test_record` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='QC 步骤结果表'; + + +CREATE TABLE `qc_photo` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `test_record_id` BIGINT NOT NULL COMMENT '关联 qc_test_record.id', + `test_no` VARCHAR(32) NOT NULL COMMENT '测试单号', + `url` VARCHAR(512) NOT NULL COMMENT '照片远程 URL', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_test_no` (`test_no`), + CONSTRAINT `fk_photo_record` FOREIGN KEY (`test_record_id`) + REFERENCES `qc_test_record` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='QC 测试照片表'; + + +-- ============================================================ +-- 示例数据(可选) +-- ============================================================ + +-- 注册一批无线板 +-- INSERT INTO `wireless_board` (`mac`) VALUES +-- ('AA:BB:CC:DD:EE:01'), +-- ('AA:BB:CC:DD:EE:02'), +-- ('AA:BB:CC:DD:EE:03'); diff --git a/src/main/java/com/corewing/app/config/SaTokenConfig.java b/src/main/java/com/corewing/app/config/SaTokenConfig.java index 26d09df..1641897 100644 --- a/src/main/java/com/corewing/app/config/SaTokenConfig.java +++ b/src/main/java/com/corewing/app/config/SaTokenConfig.java @@ -30,6 +30,8 @@ public class SaTokenConfig implements WebMvcConfigurer { .excludePathPatterns("/user/login", "/user/register", "/user/sendCode", "/user/forgetPassword", "/user/codeLogin") // 排除后台管理登录接口 .excludePathPatterns("/sys/user/login") + //排除QC接口 + .excludePathPatterns("/api/qc/**") // 排除反馈接口(支持匿名提交) .excludePathPatterns("/feedback", "/feedback/**") // 排除教程接口(支持匿名查询) diff --git a/src/main/java/com/corewing/app/dto/qc/StepResultData.java b/src/main/java/com/corewing/app/dto/qc/StepResultData.java new file mode 100644 index 0000000..d713694 --- /dev/null +++ b/src/main/java/com/corewing/app/dto/qc/StepResultData.java @@ -0,0 +1,19 @@ +package com.corewing.app.dto.qc; + +import lombok.Data; + +@Data +public class StepResultData { + + private Integer stepIndex; + + private String stepName; + + private String result; + + private String dataJson; + + private Long duration; + + private String completedAt; +} diff --git a/src/main/java/com/corewing/app/dto/qc/UploadTestRecordRequest.java b/src/main/java/com/corewing/app/dto/qc/UploadTestRecordRequest.java new file mode 100644 index 0000000..263c6bb --- /dev/null +++ b/src/main/java/com/corewing/app/dto/qc/UploadTestRecordRequest.java @@ -0,0 +1,35 @@ +package com.corewing.app.dto.qc; + +import lombok.Data; + +import java.util.List; + +@Data +public class UploadTestRecordRequest { + + private String testNo; + + private String productType; + + private String sn; + + private String operatorId; + + private String operatorName; + + private String phoneModel; + + private String appVersion; + + private String status; + + private String createTime; + + private String updatedAt; + + private String uploadTime; + + private List steps; + + private List photoUrls; +} diff --git a/src/main/java/com/corewing/app/dto/qc/ValidateSnRequest.java b/src/main/java/com/corewing/app/dto/qc/ValidateSnRequest.java new file mode 100644 index 0000000..d8f85e9 --- /dev/null +++ b/src/main/java/com/corewing/app/dto/qc/ValidateSnRequest.java @@ -0,0 +1,9 @@ +package com.corewing.app.dto.qc; + +import lombok.Data; + +@Data +public class ValidateSnRequest { + + private String sn; +} diff --git a/src/main/java/com/corewing/app/dto/qc/ValidateWirelessBoardRequest.java b/src/main/java/com/corewing/app/dto/qc/ValidateWirelessBoardRequest.java new file mode 100644 index 0000000..ac4d460 --- /dev/null +++ b/src/main/java/com/corewing/app/dto/qc/ValidateWirelessBoardRequest.java @@ -0,0 +1,8 @@ +package com.corewing.app.dto.qc; + +import lombok.Data; + +@Data +public class ValidateWirelessBoardRequest { + private String mac; +} diff --git a/src/main/java/com/corewing/app/entity/FlightController.java b/src/main/java/com/corewing/app/entity/FlightController.java new file mode 100644 index 0000000..042608b --- /dev/null +++ b/src/main/java/com/corewing/app/entity/FlightController.java @@ -0,0 +1,28 @@ +package com.corewing.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("flight_controller") +public class FlightController { + + @TableId(type = IdType.AUTO) + private Long id; + + private String sn; + + private String model; + + private String batchNo; + + private String status; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/corewing/app/entity/QcPhoto.java b/src/main/java/com/corewing/app/entity/QcPhoto.java new file mode 100644 index 0000000..1f1e376 --- /dev/null +++ b/src/main/java/com/corewing/app/entity/QcPhoto.java @@ -0,0 +1,24 @@ +package com.corewing.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("qc_photo") +public class QcPhoto { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long testRecordId; + + private String testNo; + + private String url; + + private LocalDateTime createTime; +} diff --git a/src/main/java/com/corewing/app/entity/QcStepResult.java b/src/main/java/com/corewing/app/entity/QcStepResult.java new file mode 100644 index 0000000..84ce335 --- /dev/null +++ b/src/main/java/com/corewing/app/entity/QcStepResult.java @@ -0,0 +1,30 @@ +package com.corewing.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("qc_step_result") +public class QcStepResult { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long testRecordId; + + private Integer stepIndex; + + private String stepName; + + private String result; + + private String dataJson; + + private Long duration; + + private LocalDateTime completedAt; +} diff --git a/src/main/java/com/corewing/app/entity/QcTestRecord.java b/src/main/java/com/corewing/app/entity/QcTestRecord.java new file mode 100644 index 0000000..36ada40 --- /dev/null +++ b/src/main/java/com/corewing/app/entity/QcTestRecord.java @@ -0,0 +1,42 @@ +package com.corewing.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("qc_test_record") +public class QcTestRecord { + + @TableId(type = IdType.AUTO) + private Long id; + + private String testNo; + + private String productType; + + private String sn; + + private String operatorId; + + private String operatorName; + + private String phoneModel; + + private String appVersion; + + private String status; + + private LocalDateTime createTime; + + private LocalDateTime updatedAt; + + private LocalDateTime uploadTime; + + private String reportUrl; + + private LocalDateTime serverCreateTime; +} diff --git a/src/main/java/com/corewing/app/mapper/FlightControllerMapper.java b/src/main/java/com/corewing/app/mapper/FlightControllerMapper.java new file mode 100644 index 0000000..4f3d8a5 --- /dev/null +++ b/src/main/java/com/corewing/app/mapper/FlightControllerMapper.java @@ -0,0 +1,9 @@ +package com.corewing.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.corewing.app.entity.FlightController; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FlightControllerMapper extends BaseMapper { +} diff --git a/src/main/java/com/corewing/app/mapper/QcPhotoMapper.java b/src/main/java/com/corewing/app/mapper/QcPhotoMapper.java new file mode 100644 index 0000000..5525a7a --- /dev/null +++ b/src/main/java/com/corewing/app/mapper/QcPhotoMapper.java @@ -0,0 +1,9 @@ +package com.corewing.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.corewing.app.entity.QcPhoto; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QcPhotoMapper extends BaseMapper { +} diff --git a/src/main/java/com/corewing/app/mapper/QcStepResultMapper.java b/src/main/java/com/corewing/app/mapper/QcStepResultMapper.java new file mode 100644 index 0000000..508d529 --- /dev/null +++ b/src/main/java/com/corewing/app/mapper/QcStepResultMapper.java @@ -0,0 +1,9 @@ +package com.corewing.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.corewing.app.entity.QcStepResult; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QcStepResultMapper extends BaseMapper { +} diff --git a/src/main/java/com/corewing/app/mapper/QcTestRecordMapper.java b/src/main/java/com/corewing/app/mapper/QcTestRecordMapper.java new file mode 100644 index 0000000..fad1a17 --- /dev/null +++ b/src/main/java/com/corewing/app/mapper/QcTestRecordMapper.java @@ -0,0 +1,9 @@ +package com.corewing.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.corewing.app.entity.QcTestRecord; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QcTestRecordMapper extends BaseMapper { +} diff --git a/src/main/java/com/corewing/app/modules/qc/QcConfigController.java b/src/main/java/com/corewing/app/modules/qc/QcConfigController.java new file mode 100644 index 0000000..36fe95f --- /dev/null +++ b/src/main/java/com/corewing/app/modules/qc/QcConfigController.java @@ -0,0 +1,27 @@ +package com.corewing.app.modules.qc; + +import com.corewing.app.common.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@Api(tags = "QC配置") +@RestController +@RequestMapping("/api/qc/config") +public class QcConfigController { + + @ApiOperation("获取测试配置") + @GetMapping("/test-params") + public Result> getTestParams(@RequestParam("productType") String productType) { + Map params = new HashMap<>(); + // 后续可根据 productType 返回不同的测试参数/阈值 + params.put("productType", productType); + return Result.success(params); + } +} diff --git a/src/main/java/com/corewing/app/modules/qc/QcTestController.java b/src/main/java/com/corewing/app/modules/qc/QcTestController.java new file mode 100644 index 0000000..63685ca --- /dev/null +++ b/src/main/java/com/corewing/app/modules/qc/QcTestController.java @@ -0,0 +1,57 @@ +package com.corewing.app.modules.qc; + +import com.corewing.app.common.Result; +import com.corewing.app.dto.qc.UploadTestRecordRequest; +import com.corewing.app.dto.qc.ValidateSnRequest; +import com.corewing.app.entity.QcTestRecord; +import com.corewing.app.service.QcTestService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; + +@Api(tags = "QC测试") +@RestController +@RequestMapping("/api/qc/test") +public class QcTestController { + + private final QcTestService qcTestService; + + public QcTestController(QcTestService qcTestService) { + this.qcTestService = qcTestService; + } + + @ApiOperation("上传测试记录") + @PostMapping("/upload") + public Result uploadTestRecord(@RequestBody UploadTestRecordRequest request) { + return qcTestService.uploadTestRecord(request); + } + + @ApiOperation("上传照片") + @PostMapping("/upload-photo") + public Result> uploadPhoto(@RequestParam("testNo") String testNo, + @RequestParam("photo") MultipartFile photo) { + return qcTestService.uploadPhoto(testNo, photo); + } + + @ApiOperation("验证SN") + @PostMapping("/validate-sn") + public Result validateSn(@RequestBody ValidateSnRequest request) { + return qcTestService.validateSn(request); + } + + @ApiOperation("查询SN历史记录") + @GetMapping("/history") + public Result> getHistory(@RequestParam("sn") String sn) { + return qcTestService.getHistory(sn); + } + + @ApiOperation("获取测试报告") + @GetMapping("/report") + public Result getReport(@RequestParam("testNo") String testNo) { + return qcTestService.getReport(testNo); + } +} diff --git a/src/main/java/com/corewing/app/modules/qc/WirelessBoardController.java b/src/main/java/com/corewing/app/modules/qc/WirelessBoardController.java new file mode 100644 index 0000000..83ce2f2 --- /dev/null +++ b/src/main/java/com/corewing/app/modules/qc/WirelessBoardController.java @@ -0,0 +1,29 @@ +package com.corewing.app.modules.qc; + +import com.corewing.app.common.Result; +import com.corewing.app.dto.qc.ValidateWirelessBoardRequest; +import com.corewing.app.service.BizDeviceService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Api(tags = "无线版QC") +@RestController +@RequestMapping("/api/qc/wireless") +public class WirelessBoardController { + private final BizDeviceService bizDeviceService; + + public WirelessBoardController(BizDeviceService bizDeviceService) { + this.bizDeviceService = bizDeviceService; + } + + + @ApiOperation("验证已经激活的无线板") + @PostMapping("/validate-wireless") + public Result validateWirelessBoard(@RequestBody ValidateWirelessBoardRequest validateWirelessBoardRequest) { + return bizDeviceService.validateWirelessBoard(validateWirelessBoardRequest); + } +} diff --git a/src/main/java/com/corewing/app/service/BizDeviceService.java b/src/main/java/com/corewing/app/service/BizDeviceService.java index 628599a..e489fa4 100644 --- a/src/main/java/com/corewing/app/service/BizDeviceService.java +++ b/src/main/java/com/corewing/app/service/BizDeviceService.java @@ -1,7 +1,10 @@ package com.corewing.app.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.corewing.app.common.Result; +import com.corewing.app.dto.qc.ValidateWirelessBoardRequest; import com.corewing.app.entity.BizDevice; public interface BizDeviceService extends IService { + Result validateWirelessBoard(ValidateWirelessBoardRequest validateWirelessBoardRequest); } diff --git a/src/main/java/com/corewing/app/service/QcTestService.java b/src/main/java/com/corewing/app/service/QcTestService.java new file mode 100644 index 0000000..1165ae6 --- /dev/null +++ b/src/main/java/com/corewing/app/service/QcTestService.java @@ -0,0 +1,24 @@ +package com.corewing.app.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.corewing.app.common.Result; +import com.corewing.app.dto.qc.UploadTestRecordRequest; +import com.corewing.app.dto.qc.ValidateSnRequest; +import com.corewing.app.entity.QcTestRecord; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; + +public interface QcTestService extends IService { + + Result uploadTestRecord(UploadTestRecordRequest request); + + Result> uploadPhoto(String testNo, MultipartFile photo); + + Result validateSn(ValidateSnRequest request); + + Result> getHistory(String sn); + + Result getReport(String testNo); +} diff --git a/src/main/java/com/corewing/app/service/impl/BizDeviceActivationServiceImpl.java b/src/main/java/com/corewing/app/service/impl/BizDeviceActivationServiceImpl.java index f928218..cb489d4 100644 --- a/src/main/java/com/corewing/app/service/impl/BizDeviceActivationServiceImpl.java +++ b/src/main/java/com/corewing/app/service/impl/BizDeviceActivationServiceImpl.java @@ -2,7 +2,6 @@ package com.corewing.app.service.impl; import cn.dev33.satoken.stp.StpUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.corewing.app.dto.DeviceActivationRequest; import com.corewing.app.entity.BizDevice; @@ -36,9 +35,9 @@ public class BizDeviceActivationServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BizDeviceActivation::getDeviceMac, deviceActivationRequest.getMac()); BizDeviceActivation checkDeviceActivation = getOne(wrapper); - if(checkDeviceActivation != null) { + if (checkDeviceActivation != null) { // 该用户已经激活过该设备 - if(checkDeviceActivation.getUserId().equals(loginId)) { + if (checkDeviceActivation.getUserId().equals(loginId)) { return true; } return updateById(checkDeviceActivation); @@ -48,7 +47,7 @@ public class BizDeviceActivationServiceImpl extends ServiceImpl checkDeviceWrapper = new LambdaQueryWrapper<>(); checkDeviceWrapper.eq(BizDevice::getDeviceMac, deviceActivationRequest.getMac()); List list = deviceService.list(checkDeviceWrapper); - if(list.isEmpty()) { + if (list.isEmpty()) { throw new RuntimeException("该设备不是酷翼官方产品"); } @@ -67,10 +66,8 @@ public class BizDeviceActivationServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(BizDeviceActivation::getDeviceMac, deviceActivationRequest.getMac()); - BizDeviceActivation checkDeviceActivation = getOne(wrapper); - if(checkDeviceActivation != null) { + BizDeviceActivation checkDeviceActivation = getBizDeviceActivationByMac(deviceActivationRequest.getMac()); + if (checkDeviceActivation != null) { return false; } @@ -85,4 +82,16 @@ public class BizDeviceActivationServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(BizDeviceActivation::getDeviceMac, mac); + return getOne(wrapper); + } } diff --git a/src/main/java/com/corewing/app/service/impl/BizDeviceServiceImpl.java b/src/main/java/com/corewing/app/service/impl/BizDeviceServiceImpl.java index dfa1a9f..690803a 100644 --- a/src/main/java/com/corewing/app/service/impl/BizDeviceServiceImpl.java +++ b/src/main/java/com/corewing/app/service/impl/BizDeviceServiceImpl.java @@ -1,6 +1,9 @@ package com.corewing.app.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.corewing.app.common.Result; +import com.corewing.app.dto.qc.ValidateWirelessBoardRequest; import com.corewing.app.entity.BizDevice; import com.corewing.app.mapper.BizDeviceMapper; import com.corewing.app.service.BizDeviceService; @@ -8,4 +11,16 @@ import org.springframework.stereotype.Service; @Service public class BizDeviceServiceImpl extends ServiceImpl implements BizDeviceService { + @Override + public Result validateWirelessBoard(ValidateWirelessBoardRequest validateWirelessBoardRequest) { + + LambdaQueryWrapper bizDeviceLambdaQueryWrapper = new LambdaQueryWrapper<>(); + bizDeviceLambdaQueryWrapper.eq(BizDevice::getDeviceMac, validateWirelessBoardRequest.getMac()); + long deviceCount = count(bizDeviceLambdaQueryWrapper); + if (deviceCount > 0) { + return Result.success(true); + } + + return Result.error("设备不存在或未激活"); + } } diff --git a/src/main/java/com/corewing/app/service/impl/QcTestServiceImpl.java b/src/main/java/com/corewing/app/service/impl/QcTestServiceImpl.java new file mode 100644 index 0000000..bf17dcd --- /dev/null +++ b/src/main/java/com/corewing/app/service/impl/QcTestServiceImpl.java @@ -0,0 +1,241 @@ +package com.corewing.app.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.corewing.app.common.Result; +import com.corewing.app.dto.qc.StepResultData; +import com.corewing.app.dto.qc.UploadTestRecordRequest; +import com.corewing.app.dto.qc.ValidateSnRequest; +import com.corewing.app.entity.FlightController; +import com.corewing.app.entity.QcPhoto; +import com.corewing.app.entity.QcStepResult; +import com.corewing.app.entity.QcTestRecord; +import com.corewing.app.mapper.FlightControllerMapper; +import com.corewing.app.mapper.QcPhotoMapper; +import com.corewing.app.mapper.QcStepResultMapper; +import com.corewing.app.mapper.QcTestRecordMapper; +import com.corewing.app.service.QcTestService; +import com.corewing.app.util.OSSUploadUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Service +public class QcTestServiceImpl extends ServiceImpl implements QcTestService { + + @Resource + private QcStepResultMapper qcStepResultMapper; + + @Resource + private QcPhotoMapper qcPhotoMapper; + + @Resource + private FlightControllerMapper flightControllerMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Result uploadTestRecord(UploadTestRecordRequest request) { + if (request.getTestNo() == null || request.getTestNo().isEmpty()) { + return Result.error("测试单号不能为空"); + } + + // 查找是否已存在相同 testNo 的记录 + LambdaQueryWrapper testNoQuery = new LambdaQueryWrapper<>(); + testNoQuery.eq(QcTestRecord::getTestNo, request.getTestNo()); + QcTestRecord existByTestNo = baseMapper.selectOne(testNoQuery); + + QcTestRecord record; + if (existByTestNo != null) { + // 相同 testNo 已存在 → 更新(APP 重传场景) + record = existByTestNo; + fillRecord(record, request); + baseMapper.updateById(record); + // 清除旧的步骤和照片,后面重新插入 + deleteStepsAndPhotos(record.getId()); + } else { + // 查找同一 SN 是否已有记录(返修重测场景) + LambdaQueryWrapper snQuery = new LambdaQueryWrapper<>(); + snQuery.eq(QcTestRecord::getSn, request.getSn()) + .eq(QcTestRecord::getProductType, request.getProductType()) + .orderByDesc(QcTestRecord::getCreateTime) + .last("LIMIT 1"); + QcTestRecord existBySn = baseMapper.selectOne(snQuery); + + if (existBySn != null) { + // 同一 SN 已有记录 → 更新最近一条(返修后重新 QC) + record = existBySn; + fillRecord(record, request); + baseMapper.updateById(record); + deleteStepsAndPhotos(record.getId()); + } else { + // 全新记录 + record = new QcTestRecord(); + fillRecord(record, request); + baseMapper.insert(record); + } + } + + // 保存步骤结果 + if (!CollectionUtils.isEmpty(request.getSteps())) { + for (StepResultData stepData : request.getSteps()) { + QcStepResult step = new QcStepResult(); + BeanUtils.copyProperties(stepData, step, "completedAt"); + step.setTestRecordId(record.getId()); + step.setCompletedAt(parseTimestamp(stepData.getCompletedAt())); + qcStepResultMapper.insert(step); + } + } + + // 保存照片 URL + if (!CollectionUtils.isEmpty(request.getPhotoUrls())) { + for (String photoUrl : request.getPhotoUrls()) { + QcPhoto photo = new QcPhoto(); + photo.setTestRecordId(record.getId()); + photo.setTestNo(request.getTestNo()); + photo.setUrl(photoUrl); + qcPhotoMapper.insert(photo); + } + } + + // 返回报告 URL(可为 null) + return Result.success("上传成功", record.getReportUrl()); + } + + private void fillRecord(QcTestRecord record, UploadTestRecordRequest request) { + // 拷贝同名同类型字段(testNo, productType, sn, operatorId, operatorName, phoneModel, appVersion, status) + BeanUtils.copyProperties(request, record, "createTime", "updatedAt", "uploadTime"); + // 时间戳字符串 → LocalDateTime 需要手动转换 + record.setCreateTime(parseTimestamp(request.getCreateTime())); + record.setUpdatedAt(parseTimestamp(request.getUpdatedAt())); + record.setUploadTime(parseTimestamp(request.getUploadTime())); + } + + private void deleteStepsAndPhotos(Long recordId) { + LambdaQueryWrapper stepDelete = new LambdaQueryWrapper<>(); + stepDelete.eq(QcStepResult::getTestRecordId, recordId); + qcStepResultMapper.delete(stepDelete); + + LambdaQueryWrapper photoDelete = new LambdaQueryWrapper<>(); + photoDelete.eq(QcPhoto::getTestRecordId, recordId); + qcPhotoMapper.delete(photoDelete); + } + + @Override + public Result> uploadPhoto(String testNo, MultipartFile photo) { + if (photo == null || photo.isEmpty()) { + return Result.error("照片文件不能为空"); + } + if (testNo == null || testNo.isEmpty()) { + return Result.error("测试单号不能为空"); + } + + // 查找对应的测试记录 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(QcTestRecord::getTestNo, testNo); + QcTestRecord record = baseMapper.selectOne(query); + if (record == null) { + return Result.error("测试记录不存在: " + testNo); + } + + try { + // 生成 OSS 存储路径 + String originalFilename = photo.getOriginalFilename(); + String suffix = ""; + if (originalFilename != null && originalFilename.contains(".")) { + suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + String objectName = "qc/photos/" + testNo + "/" + UUID.randomUUID().toString().replace("-", "") + suffix; + + // 上传到 OSS + String url = OSSUploadUtil.uploadFile(photo.getInputStream(), objectName); + + // 保存照片记录 + QcPhoto qcPhoto = new QcPhoto(); + qcPhoto.setTestRecordId(record.getId()); + qcPhoto.setTestNo(testNo); + qcPhoto.setUrl(url); + qcPhotoMapper.insert(qcPhoto); + + Map result = new HashMap<>(); + result.put("url", url); + return Result.success("上传成功", result); + } catch (Exception e) { + log.error("照片上传失败", e); + return Result.error("照片上传失败: " + e.getMessage()); + } + } + + @Override + public Result validateSn(ValidateSnRequest request) { + if (request.getSn() == null || request.getSn().isEmpty()) { + return Result.error("SN 不能为空"); + } + + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FlightController::getSn, request.getSn()); + Long count = flightControllerMapper.selectCount(query); + + if (count > 0) { + return Result.success("SN 有效", true); + } + + return Result.error("SN 无效或未注册"); + } + + @Override + public Result> getHistory(String sn) { + if (sn == null || sn.isEmpty()) { + return Result.error("SN 不能为空"); + } + + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(QcTestRecord::getSn, sn) + .orderByDesc(QcTestRecord::getCreateTime); + List records = baseMapper.selectList(query); + + return Result.success(records); + } + + @Override + public Result getReport(String testNo) { + if (testNo == null || testNo.isEmpty()) { + return Result.error("测试单号不能为空"); + } + + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(QcTestRecord::getTestNo, testNo); + QcTestRecord record = baseMapper.selectOne(query); + + if (record == null) { + return Result.error("测试记录不存在"); + } + + return Result.success(record.getReportUrl()); + } + + private LocalDateTime parseTimestamp(String timestamp) { + if (timestamp == null || timestamp.isEmpty()) { + return null; + } + try { + long millis = Long.parseLong(timestamp); + if (millis <= 0) { + return null; + } + return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault()); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/src/main/resources/db/qc.sql b/src/main/resources/db/qc.sql new file mode 100644 index 0000000..a1dc9aa --- /dev/null +++ b/src/main/resources/db/qc.sql @@ -0,0 +1,81 @@ +-- ============================================================ +-- Corewing QC 后端数据库建表 SQL(MySQL) +-- ============================================================ + + +CREATE TABLE `flight_controller` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `sn` VARCHAR(64) NOT NULL COMMENT '序列号', + `model` VARCHAR(64) DEFAULT NULL COMMENT '型号', + `batch_no` VARCHAR(64) DEFAULT NULL COMMENT '批次号', + `status` VARCHAR(32) NOT NULL DEFAULT 'REGISTERED' COMMENT '状态: REGISTERED / QC_PASS / QC_FAIL / ACTIVATED', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_sn` (`sn`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='飞控板设备注册表'; + + +CREATE TABLE `qc_test_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `test_no` VARCHAR(32) NOT NULL COMMENT '测试单号,格式: WL-yyyyMMdd-NNN / FC-yyyyMMdd-NNN', + `product_type` VARCHAR(32) NOT NULL COMMENT '产品类型: WIRELESS_BOARD / FLIGHT_CONTROLLER', + `sn` VARCHAR(64) NOT NULL COMMENT '产品序列号(无线板为 BLE MAC)', + `operator_id` VARCHAR(32) NOT NULL COMMENT '测试员 ID', + `operator_name` VARCHAR(64) NOT NULL COMMENT '测试员姓名', + `phone_model` VARCHAR(64) DEFAULT NULL COMMENT '测试手机型号', + `app_version` VARCHAR(16) DEFAULT NULL COMMENT 'APP 版本', + `status` VARCHAR(16) NOT NULL COMMENT '测试结果: PASS / FAIL', + `create_time` DATETIME NOT NULL COMMENT 'APP 端创建时间', + `updated_at` DATETIME DEFAULT NULL COMMENT 'APP 端最后更新时间', + `upload_time` DATETIME DEFAULT NULL COMMENT 'APP 端上传时间', + `report_url` VARCHAR(512) DEFAULT NULL COMMENT '测试报告 URL', + `server_create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '服务端入库时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_test_no` (`test_no`), + KEY `idx_sn` (`sn`), + KEY `idx_product_type` (`product_type`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='QC 测试记录主表'; + + +CREATE TABLE `qc_step_result` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `test_record_id` BIGINT NOT NULL COMMENT '关联 qc_test_record.id', + `step_index` INT NOT NULL COMMENT '步骤序号(从 1 开始)', + `step_name` VARCHAR(64) NOT NULL COMMENT '步骤名称', + `result` VARCHAR(8) DEFAULT NULL COMMENT '步骤结果: PASS / FAIL / NULL', + `data_json` TEXT DEFAULT NULL COMMENT '步骤详细数据 JSON', + `duration` BIGINT DEFAULT 0 COMMENT '步骤耗时(毫秒)', + `completed_at` DATETIME DEFAULT NULL COMMENT '步骤完成时间', + PRIMARY KEY (`id`), + KEY `idx_test_record_id` (`test_record_id`), + UNIQUE KEY `uk_record_step` (`test_record_id`, `step_index`), + CONSTRAINT `fk_step_record` FOREIGN KEY (`test_record_id`) + REFERENCES `qc_test_record` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='QC 步骤结果表'; + + +CREATE TABLE `qc_photo` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `test_record_id` BIGINT NOT NULL COMMENT '关联 qc_test_record.id', + `test_no` VARCHAR(32) NOT NULL COMMENT '测试单号', + `url` VARCHAR(512) NOT NULL COMMENT '照片远程 URL', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_test_no` (`test_no`), + CONSTRAINT `fk_photo_record` FOREIGN KEY (`test_record_id`) + REFERENCES `qc_test_record` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='QC 测试照片表'; + + +-- ============================================================ +-- 示例数据(可选) +-- ============================================================ + +-- 注册一批无线板 +-- INSERT INTO `wireless_board` (`mac`) VALUES +-- ('AA:BB:CC:DD:EE:01'), +-- ('AA:BB:CC:DD:EE:02'), +-- ('AA:BB:CC:DD:EE:03');