已添加5个文件
已修改4个文件
772 ■■■■■ 文件已修改
doc/20260604_common_file迁移到storage_blob_storage_attachment.sql 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260604_物联设备接口新增存放位置字段前端联调文档.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/update_blob_file.sh 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/update_file_size.sh 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/controller/JclyController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/QualityInspectTemplateExportHelper.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/百事模版模板说明.md 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260604_common_fileÇ¨ÒÆµ½storage_blob_storage_attachment.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,283 @@
-- ============================================================
-- é™„件数据迁移脚本:从 common_file è¿ç§»åˆ° storage_blob + storage_attachment
-- æ‰§è¡Œå‰è¯·å¤‡ä»½æ•°æ®ï¼
-- ============================================================
-- è¯´æ˜Žï¼š
-- 1. common_file è¡¨ï¼šæ—§é™„件表,包含文件信息
-- 2. storage_blob è¡¨ï¼šæ–°æ–‡ä»¶å®žä½“表,存储文件元信息
-- 3. storage_attachment è¡¨ï¼šæ–°é™„件关联表,关联业务记录与文件
-- æ—§è¡¨å­—段说明:
-- common_file.id         - ä¸»é”®ID
-- common_file.common_id  - å…³è”业务ID
-- common_file.name       - æ–‡ä»¶åç§°
-- common_file.url        - æ–‡ä»¶è·¯å¾„
-- common_file.type       - ä¸šåŠ¡ç±»åž‹ï¼ˆæ•´æ•°æžšä¸¾ï¼‰
-- common_file.file_size  - æ–‡ä»¶å¤§å°
-- common_file.create_time, update_time, create_user, dept_id
-- æ–°è¡¨å­—段说明:
-- storage_blob.id               - ä¸»é”®ID
-- storage_blob.resource_key     - èµ„源唯一标识(可用原ID或UUID)
-- storage_blob.content_type     - MIME类型(需根据文件扩展名推断)
-- storage_blob.original_filename- åŽŸå§‹æ–‡ä»¶å
-- storage_blob.uid_filename     - å”¯ä¸€æ–‡ä»¶åï¼ˆå¯ç”¨åŽŸname或生成)
-- storage_blob.byte_size        - æ–‡ä»¶å¤§å°
-- storage_blob.path             - æ–‡ä»¶è·¯å¾„
-- storage_attachment.id         - ä¸»é”®ID
-- storage_attachment.record_type- è®°å½•类型(字符串枚举)
-- storage_attachment.record_id  - å…³è”业务ID
-- storage_attachment.application- æ–‡ä»¶ç”¨é€”(默认 'file')
-- storage_attachment.storage_blob_id - å…³è” storage_blob.id
-- storage_attachment.deleted    - é€»è¾‘删除标记
-- storage_attachment.create_time, update_time
-- ============================================================
-- ç±»åž‹æ˜ å°„:旧 type(int) -> æ–° record_type(string)
-- æ ¹æ® FileNameType æžšä¸¾å®šä¹‰æ˜ å°„关系
-- ============================================================
-- æ—§ç±»åž‹æžšä¸¾å€¼ï¼š
-- 1  = SALE (销售)
-- 2  = PURCHASE (采购)
-- 3  = INVOICE (发票)
-- 4  = PURCHASELEDGER (采购台账)
-- 5  = MEASURING (计量器具台账)
-- 6  = MEASURINGRecord (计量器具台账记录)
-- 7  = ApproveNode (协同审批节点审核)
-- 8  = ApproveProcess (协同审批主数据)
-- 9  = SHIP (发货台账)
-- 10 = INSPECTION_PRODUCTION_BEFORE (生产前巡检)
-- 11 = INSPECTION_PRODUCTION_AFTER (生产后巡检)
-- 12 = INSPECTION (巡检)
-- 13 = APP
-- æ–°ç±»åž‹å¯¹åº”çš„ record_type å­—符串(根据 RecordTypeEnum æŽ¨æ–­ï¼‰ï¼š
-- 1  -> 'sales_ledger'
-- 2  -> 'purchase_ledger'
-- 3  -> 'invoice_ledger'
-- 4  -> 'purchase_ledger_file'
-- 5  -> 'measuring_instrument_ledger'
-- 6  -> 'measuring_instrument_ledger_record'
-- 7  -> 'approve_node'
-- 8  -> 'approve_process'
-- 9  -> 'shipping_info'
-- 10 -> 'inspection_task'
-- 11 -> 'inspection_task'
-- 12 -> 'inspection_task'
-- 13 -> 'common_file'(通用类型兜底)
-- ============================================================
-- æ­¥éª¤1:迁移文件数据到 storage_blob
-- æ³¨æ„ï¼šbyte_size å…ˆä½¿ç”¨ common_file.file_size,后续需要在服务器上更新实际大小
-- ============================================================
INSERT INTO storage_blob (
    resource_key,
    content_type,
    original_filename,
    uid_filename,
    byte_size,
    path
)
SELECT
    CONCAT('legacy_', cf.id) AS resource_key,
    CASE
        -- æ ¹æ®æ–‡ä»¶æ‰©å±•名推断 MIME ç±»åž‹
        WHEN LOWER(cf.name) LIKE '%.jpg' OR LOWER(cf.name) LIKE '%.jpeg' THEN 'image/jpeg'
        WHEN LOWER(cf.name) LIKE '%.png' THEN 'image/png'
        WHEN LOWER(cf.name) LIKE '%.gif' THEN 'image/gif'
        WHEN LOWER(cf.name) LIKE '%.bmp' THEN 'image/bmp'
        WHEN LOWER(cf.name) LIKE '%.webp' THEN 'image/webp'
        WHEN LOWER(cf.name) LIKE '%.pdf' THEN 'application/pdf'
        WHEN LOWER(cf.name) LIKE '%.doc' THEN 'application/msword'
        WHEN LOWER(cf.name) LIKE '%.docx' THEN 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        WHEN LOWER(cf.name) LIKE '%.xls' THEN 'application/vnd.ms-excel'
        WHEN LOWER(cf.name) LIKE '%.xlsx' THEN 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        WHEN LOWER(cf.name) LIKE '%.ppt' THEN 'application/vnd.ms-powerpoint'
        WHEN LOWER(cf.name) LIKE '%.pptx' THEN 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
        WHEN LOWER(cf.name) LIKE '%.txt' THEN 'text/plain'
        WHEN LOWER(cf.name) LIKE '%.zip' THEN 'application/zip'
        WHEN LOWER(cf.name) LIKE '%.rar' THEN 'application/x-rar-compressed'
        WHEN LOWER(cf.name) LIKE '%.mp4' THEN 'video/mp4'
        WHEN LOWER(cf.name) LIKE '%.mp3' THEN 'audio/mpeg'
        WHEN LOWER(cf.name) LIKE '%.xml' THEN 'application/xml'
        ELSE 'application/octet-stream'
    END AS content_type,
    cf.name AS original_filename,
    -- ä»Ž url ä¸­æˆªå–最后的文件名部分
    -- url ç¤ºä¾‹: /javaWork/product-inventory-management/file/prod/uploads2026-05-19/1652_1779177065596_61abcb44.jpg
    -- æˆªå–结果: 1652_1779177065596_61abcb44.jpg
    CASE
        WHEN cf.url IS NOT NULL AND cf.url != '' THEN
            SUBSTRING_INDEX(cf.url, '/', -1)
        ELSE cf.name
    END AS uid_filename,
    COALESCE(cf.file_size, 0) AS byte_size,
    cf.url AS path
FROM common_file cf
WHERE NOT EXISTS (
    SELECT 1 FROM storage_blob sb WHERE sb.resource_key = CONCAT('legacy_', cf.id)
);
-- ============================================================
-- æ­¥éª¤1.5:在Linux服务器上更新实际文件大小
-- ç›´æŽ¥å¤åˆ¶ä»¥ä¸‹å†…容到服务器执行
-- ============================================================
-- åœ¨æœåŠ¡å™¨ä¸Šæ‰§è¡Œä»¥ä¸‹å‘½ä»¤åˆ›å»ºè„šæœ¬ï¼š
-- cat > update_blob_file.sh << 'EOF'
-- #!/bin/bash
-- FILE_ROOT="/javaWork/product-inventory-management/file/prod"
-- OUTPUT_SQL="update_blob_file_size.sql"
-- DB_USER="root"
-- DB_PASS="xd@123456.."
-- DB_NAME="product_inventory_management"
-- echo "开始扫描目录: $FILE_ROOT"
-- echo "-- æ›´æ–°æ–‡ä»¶å¤§å° SQL" > "$OUTPUT_SQL"
-- find "$FILE_ROOT" -type f | while read filepath; do
--     filesize=$(stat -c%s "$filepath" 2>/dev/null || echo "0")
--     filename=$(basename "$filepath")
--     filename_escaped=$(echo "$filename" | sed "s/'/''/g")
--     echo "UPDATE storage_blob SET byte_size = $filesize WHERE uid_filename = '$filename_escaped';" >> "$OUTPUT_SQL"
-- done
-- echo "SQL文件已生成: $OUTPUT_SQL"
-- docker exec -i mysql mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$OUTPUT_SQL"
-- echo "更新完成!"
-- EOF
--
-- ç„¶åŽæ‰§è¡Œï¼š
-- chmod +x update_blob_file.sh && ./update_blob_file.sh
-- ============================================================
-- æ­¥éª¤2:迁移附件关联数据到 storage_attachment
-- ============================================================
INSERT INTO storage_attachment (
    create_time,
    update_time,
    deleted,
    record_type,
    record_id,
    application,
    storage_blob_id
)
SELECT
    cf.create_time,
    cf.update_time,
    0 AS deleted,
    CASE cf.type
        WHEN 1 THEN 'sales_ledger'
        WHEN 2 THEN 'purchase_ledger'
        WHEN 3 THEN 'invoice_ledger'
        WHEN 4 THEN 'purchase_ledger_file'
        WHEN 5 THEN 'measuring_instrument_ledger'
        WHEN 6 THEN 'measuring_instrument_ledger_record'
        WHEN 7 THEN 'approve_node'
        WHEN 8 THEN 'approve_process'
        WHEN 9 THEN 'shipping_info'
        WHEN 10 THEN 'inspection_task'
        WHEN 11 THEN 'inspection_task'
        WHEN 12 THEN 'inspection_task'
        WHEN 13 THEN 'common_file'
        ELSE 'common_file'
    END AS record_type,
    cf.common_id AS record_id,
    'file' AS application,
    sb.id AS storage_blob_id
FROM common_file cf
INNER JOIN storage_blob sb ON sb.resource_key = CONCAT('legacy_', cf.id)
WHERE NOT EXISTS (
    SELECT 1 FROM storage_attachment sa
    WHERE sa.storage_blob_id = sb.id
);
-- ============================================================
-- æ­¥éª¤3:验证迁移结果
-- ============================================================
-- æ£€æŸ¥è¿ç§»æ•°é‡æ˜¯å¦ä¸€è‡´
SELECT
    'common_file åŽŸå§‹è®°å½•æ•°' AS description,
    COUNT(*) AS count
FROM common_file
UNION ALL
SELECT
    'storage_blob è¿ç§»è®°å½•æ•°' AS description,
    COUNT(*) AS count
FROM storage_blob
WHERE resource_key LIKE 'legacy_%'
UNION ALL
SELECT
    'storage_attachment è¿ç§»è®°å½•æ•°' AS description,
    COUNT(*) AS count
FROM storage_attachment sa
WHERE EXISTS (
    SELECT 1 FROM storage_blob sb
    WHERE sb.id = sa.storage_blob_id
    AND sb.resource_key LIKE 'legacy_%'
);
-- æŒ‰ç±»åž‹ç»Ÿè®¡è¿ç§»æ•°é‡
SELECT
    cf.type AS old_type,
    CASE cf.type
        WHEN 1 THEN 'sales_ledger'
        WHEN 2 THEN 'purchase_ledger'
        WHEN 3 THEN 'invoice_ledger'
        WHEN 4 THEN 'purchase_ledger_file'
        WHEN 5 THEN 'measuring_instrument_ledger'
        WHEN 6 THEN 'measuring_instrument_ledger_record'
        WHEN 7 THEN 'approve_node'
        WHEN 8 THEN 'approve_process'
        WHEN 9 THEN 'shipping_info'
        WHEN 10 THEN 'inspection_task'
        WHEN 11 THEN 'inspection_task'
        WHEN 12 THEN 'inspection_task'
        WHEN 13 THEN 'common_file'
        ELSE 'unknown'
    END AS new_record_type,
    COUNT(*) AS count
FROM common_file cf
GROUP BY cf.type
ORDER BY cf.type;
-- ============================================================
-- æ­¥éª¤4:数据校验(执行前检查是否有异常数据)
-- ============================================================
-- æ£€æŸ¥æ˜¯å¦æœ‰ common_id ä¸ºç©ºçš„记录
SELECT COUNT(*) AS 'common_id为空的记录数' FROM common_file WHERE common_id IS NULL;
-- æ£€æŸ¥æ˜¯å¦æœ‰ name ä¸ºç©ºçš„记录
SELECT COUNT(*) AS 'name为空的记录数' FROM common_file WHERE name IS NULL OR name = '';
-- æ£€æŸ¥æ˜¯å¦æœ‰ url ä¸ºç©ºçš„记录
SELECT COUNT(*) AS 'url为空的记录数' FROM common_file WHERE url IS NULL OR url = '';
-- ============================================================
-- æ­¥éª¤5:迁移完成后的清理(谨慎执行!建议先备份数据)
-- ============================================================
-- å¤‡ä»½ common_file è¡¨ï¼ˆå¯é€‰ï¼‰
-- CREATE TABLE common_file_backup AS SELECT * FROM common_file;
-- æ¸…空 common_file è¡¨ï¼ˆç¡®è®¤è¿ç§»æ— è¯¯åŽæ‰§è¡Œï¼‰
-- TRUNCATE TABLE common_file;
-- æˆ–者删除已迁移的记录
-- DELETE FROM common_file WHERE EXISTS (
--     SELECT 1 FROM storage_blob sb
--     WHERE sb.resource_key = CONCAT('legacy_', common_file.id)
-- );
-- ============================================================
-- æ³¨æ„äº‹é¡¹ï¼š
-- 1. æ‰§è¡Œå‰åŠ¡å¿…å¤‡ä»½ common_file è¡¨æ•°æ®
-- 2. å»ºè®®åœ¨æµ‹è¯•环境先验证迁移脚本的正确性
-- 3. è¿ç§»å®ŒæˆåŽï¼Œæ£€æŸ¥æ–‡ä»¶æ˜¯å¦èƒ½æ­£å¸¸è®¿é—®
-- 4. ç¡®è®¤ä¸šåŠ¡åŠŸèƒ½æ­£å¸¸åŽï¼Œå†è€ƒè™‘æ¸…ç†æ—§æ•°æ®
-- 5. resource_key ä½¿ç”¨ 'legacy_' + åŽŸID çš„æ ¼å¼ï¼Œä¾¿äºŽè¿½æº¯å’ŒåŽ»é‡
-- ============================================================
doc/20260604_ÎïÁªÉ豸½Ó¿ÚÐÂÔö´æ·ÅλÖÃ×Ö¶Îǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
# ç‰©è”设备接口新增存放位置字段前端联调文档
## å˜æ›´æ¦‚è¿°
本次变更在两个物联设备相关接口的返回数据中新增了 `storageLocation`(存放位置)字段,供前端展示设备所在的物理位置信息。
---
## ä¸€ã€æ•°é‡‡æŽ¥å£ - getRealData
### æŽ¥å£ä¿¡æ¯
| é¡¹ç›® | å€¼ |
|------|-----|
| æŽ¥å£è·¯å¾„ | `/iot/getRealData` |
| è¯·æ±‚方式 | GET |
| æŽ¥å£æè¿° | å®žæ—¶èŽ·å–ç‰©è”è®¾å¤‡çš„æ¸©æ¹¿åº¦ã€äºŒæ°§åŒ–ç¢³ç­‰æ•°é‡‡æ•°æ® |
### è¯·æ±‚参数
无参数
### è¿”回数据结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "guid": "设备外部编码",
      "deviceName": "设备名称",
      "storageLocation": "存放位置【新增】",
      "status": "在线/离线/error",
      "statusMessage": "状态说明(仅在异常时返回)",
      "light": "光照值+单位(如:150Lux)",
      "temperature": "温度值+单位(如:25.5℃)",
      "humidity": "湿度值+单位(如:60%RH)",
      "co2": "二氧化碳值+单位(如:400ppm)",
      "battery": "电池电量+单位(如:85%)"
    }
  ]
}
```
### è¿”回字段说明
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | æ˜¯å¦æ–°å¢ž |
|--------|------|------|----------|
| guid | String | è®¾å¤‡å¤–部编码(externalCode) | å¦ |
| deviceName | String | è®¾å¤‡åç§° | å¦ |
| **storageLocation** | String | **设备存放位置** | **是** |
| status | String | è®¾å¤‡çŠ¶æ€ï¼šåœ¨çº¿ã€ç¦»çº¿ã€error | å¦ |
| statusMessage | String | çŠ¶æ€è¡¥å……è¯´æ˜Žï¼ˆä»…åœ¨å¼‚å¸¸æ—¶å­˜åœ¨ï¼‰ | å¦ |
| light | String | å…‰ç…§å€¼ï¼ˆå¸¦å•位) | å¦ |
| temperature | String | æ¸©åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| humidity | String | æ¹¿åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| co2 | String | äºŒæ°§åŒ–碳值(带单位) | å¦ |
| battery | String | ç”µæ± ç”µé‡ï¼ˆå¸¦å•位) | å¦ |
### å‰ç«¯è”调示例
```javascript
// è°ƒç”¨æŽ¥å£
axios.get('/iot/getRealData').then(res => {
  const devices = res.data;
  devices.forEach(device => {
    console.log(`设备:${device.deviceName}`);
    console.log(`存放位置:${device.storageLocation}`); // æ–°å¢žå­—段
    console.log(`温度:${device.temperature}`);
    console.log(`湿度:${device.humidity}`);
    console.log(`CO2:${device.co2}`);
    console.log(`状态:${device.status}`);
  });
});
```
---
## äºŒã€åº“存物联设备接口 - iotRealtime
### æŽ¥å£ä¿¡æ¯
| é¡¹ç›® | å€¼ |
|------|-----|
| æŽ¥å£è·¯å¾„ | `/stockInventory/iotRealtime` |
| è¯·æ±‚方式 | POST |
| Content-Type | application/json |
| æŽ¥å£æè¿° | èŽ·å–åº“å­˜ç»‘å®šçš„ç‰©è”è®¾å¤‡å®žæ—¶æ•°é‡‡æ•°æ® |
### è¯·æ±‚参数
```json
[设备ID1, è®¾å¤‡ID2, è®¾å¤‡ID3]
```
**参数说明:**
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| - | List<Long> | æ˜¯ | ç‰©è”设备ID数组 |
### è¿”回数据结构
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "devices": [
      {
        "deviceId": "设备ID",
        "deviceName": "设备名称",
        "deviceModel": "设备规格型号",
        "externalCode": "设备外部编码",
        "storageLocation": "存放位置【新增】",
        "guid": "设备外部编码",
        "status": "在线/离线/error",
        "statusMessage": "状态说明(仅在异常时返回)",
        "light": "光照值+单位",
        "temperature": "温度值+单位",
        "humidity": "湿度值+单位",
        "co2": "二氧化碳值+单位",
        "battery": "电池电量+单位"
      }
    ]
  }
}
```
### è¿”回字段说明(devices æ•°ç»„元素)
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | æ˜¯å¦æ–°å¢ž |
|--------|------|------|----------|
| deviceId | String | è®¾å¤‡ID | å¦ |
| deviceName | String | è®¾å¤‡åç§° | å¦ |
| deviceModel | String | è®¾å¤‡è§„格型号 | å¦ |
| externalCode | String | è®¾å¤‡å¤–部编码 | å¦ |
| **storageLocation** | String | **设备存放位置** | **是** |
| guid | String | è®¾å¤‡å¤–部编码(来自数采平台) | å¦ |
| status | String | è®¾å¤‡çŠ¶æ€ï¼šåœ¨çº¿ã€ç¦»çº¿ã€error | å¦ |
| statusMessage | String | çŠ¶æ€è¡¥å……è¯´æ˜Žï¼ˆä»…åœ¨å¼‚å¸¸æ—¶å­˜åœ¨ï¼‰ | å¦ |
| light | String | å…‰ç…§å€¼ï¼ˆå¸¦å•位) | å¦ |
| temperature | String | æ¸©åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| humidity | String | æ¹¿åº¦å€¼ï¼ˆå¸¦å•位) | å¦ |
| co2 | String | äºŒæ°§åŒ–碳值(带单位) | å¦ |
| battery | String | ç”µæ± ç”µé‡ï¼ˆå¸¦å•位) | å¦ |
### å‰ç«¯è”调示例
```javascript
// è°ƒç”¨æŽ¥å£
const deviceIds = [1, 2, 3];
axios.post('/stockInventory/iotRealtime', deviceIds).then(res => {
  const result = res.data;
  result.devices.forEach(device => {
    console.log(`设备ID:${device.deviceId}`);
    console.log(`设备名称:${device.deviceName}`);
    console.log(`存放位置:${device.storageLocation}`); // æ–°å¢žå­—段
    console.log(`规格型号:${device.deviceModel}`);
    console.log(`温度:${device.temperature}`);
    console.log(`湿度:${device.humidity}`);
    console.log(`状态:${device.status}`);
  });
});
```
---
## ä¸‰ã€å‰ç«¯æ”¹é€ è¦ç‚¹
### 1. åˆ—表/卡片展示改造
若前端有设备列表或卡片展示,需新增存放位置展示区域:
```html
<!-- ç¤ºä¾‹ï¼šè®¾å¤‡ä¿¡æ¯å¡ç‰‡ -->
<div class="device-card">
  <div class="device-name">{{ device.deviceName }}</div>
  <!-- æ–°å¢žå­˜æ”¾ä½ç½®å±•示 -->
  <div class="storage-location">存放位置:{{ device.storageLocation || '暂无' }}</div>
  <div class="device-status">状态:{{ device.status }}</div>
  <div class="device-data">
    <span>温度:{{ device.temperature }}</span>
    <span>湿度:{{ device.humidity }}</span>
    <span>CO2:{{ device.co2 }}</span>
  </div>
</div>
```
### 2. è¡¨æ ¼åˆ—新增
若前端使用表格展示设备数据,需新增存放位置列:
| åˆ—名 | å­—段 | è¯´æ˜Ž |
|------|------|------|
| è®¾å¤‡åç§° | deviceName | - |
| **存放位置** | **storageLocation** | **新增列** |
| æ¸©åº¦ | temperature | - |
| æ¹¿åº¦ | humidity | - |
| CO2 | co2 | - |
| çŠ¶æ€ | status | - |
### 3. ç©ºå€¼å¤„理
`storageLocation` å­—段可能为空字符串,前端需做兜底处理:
```javascript
const location = device.storageLocation || '暂无存放位置信息';
```
---
## å››ã€æ•°æ®æ¥æºè¯´æ˜Ž
`storageLocation` å­—段来源于设备台账表(`device_ledger`)的 `storage_location` å­—段,该字段在设备台账管理模块维护。
---
## äº”、测试要点
1. **正常场景**:设备台账中已维护存放位置,接口返回应包含该字段值
2. **空值场景**:设备台账中存放位置为空,接口应返回空字符串(前端需兜底展示)
3. **离线设备**:设备离线时,`storageLocation` å­—段仍应正常返回
4. **批量查询**:多个设备ID查询时,每个设备都应返回 `storageLocation` å­—段
---
## å…­ã€ç‰ˆæœ¬è®°å½•
| ç‰ˆæœ¬ | æ—¥æœŸ | å˜æ›´å†…容 |
|------|------|----------|
| v1.0 | 2026-06-04 | æ–°å¢ž storageLocation(存放位置)字段 |
doc/update_blob_file.sh
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
#!/bin/bash
# æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
FILE_ROOT="/javaWork/product-inventory-management/file/prod"
# è¾“出SQL文件名
OUTPUT_SQL="update_blob_file_size.sql"
# æ•°æ®åº“配置
DB_USER="root"
DB_PASS="xd@123456.."
DB_NAME="product_inventory_management"
echo "开始扫描目录: $FILE_ROOT"
echo "-- æ›´æ–°æ–‡ä»¶å¤§å° SQL" > "$OUTPUT_SQL"
echo "-- ç”Ÿæˆæ—¶é—´: $(date '+%Y-%m-%d %H:%M:%S')" >> "$OUTPUT_SQL"
echo "" >> "$OUTPUT_SQL"
# æ‰«æç›®å½•下所有文件并生成更新SQL
find "$FILE_ROOT" -type f | while read filepath; do
    filesize=$(stat -c%s "$filepath" 2>/dev/null || echo "0")
    filename=$(basename "$filepath")
    filename_escaped=$(echo "$filename" | sed "s/'/''/g")
    echo "UPDATE storage_blob SET byte_size = $filesize WHERE uid_filename = '$filename_escaped';" >> "$OUTPUT_SQL"
done
echo "SQL文件已生成: $OUTPUT_SQL"
echo "正在执行SQL更新..."
docker exec -i mysql mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$OUTPUT_SQL"
echo "更新完成!"
doc/update_file_size.sh
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
#!/bin/bash
# ============================================================
# æ›´æ–° storage_blob è¡¨çš„æ–‡ä»¶å¤§å°å­—段
# æ‰«ææ–‡ä»¶ç›®å½•获取实际文件大小,生成更新SQL
# ============================================================
# æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•(根据实际路径修改)
FILE_ROOT="/javaWork/product-inventory-management/file/prod"
# è¾“出SQL文件名
OUTPUT_SQL="update_blob_file_size.sql"
echo "开始扫描目录: $FILE_ROOT"
echo "-- æ›´æ–°æ–‡ä»¶å¤§å° SQL" > "$OUTPUT_SQL"
echo "-- ç”Ÿæˆæ—¶é—´: $(date '+%Y-%m-%d %H:%M:%S')" >> "$OUTPUT_SQL"
echo "" >> "$OUTPUT_SQL"
# ç»Ÿè®¡è®¡æ•°
count=0
# æ‰«æç›®å½•下所有文件并生成更新SQL
find "$FILE_ROOT" -type f | while read filepath; do
    # èŽ·å–æ–‡ä»¶å¤§å°ï¼ˆå­—èŠ‚ï¼‰
    filesize=$(stat -c%s "$filepath" 2>/dev/null || echo "0")
    # èŽ·å–æ–‡ä»¶åï¼ˆæœ€åŽä¸€æ®µè·¯å¾„ï¼‰
    filename=$(basename "$filepath")
    # è½¬ä¹‰æ–‡ä»¶åä¸­çš„单引号(SQL语法要求)
    filename_escaped=$(echo "$filename" | sed "s/'/''/g")
    # ç”Ÿæˆæ›´æ–°SQL
    echo "UPDATE storage_blob SET byte_size = $filesize WHERE uid_filename = '$filename_escaped';" >> "$OUTPUT_SQL"
    # è®¡æ•°ï¼ˆå­shell中无法传递,仅用于显示进度)
    echo "已处理: $filename ($filesize bytes)"
done
echo ""
echo "=========================================="
echo "SQL文件已生成: $OUTPUT_SQL"
echo "=========================================="
echo ""
echo "执行方法:"
echo "  mysql -u用户名 -p密码 æ•°æ®åº“名 < $OUTPUT_SQL"
echo ""
echo "或者先检查SQL内容:"
echo "  cat $OUTPUT_SQL"
src/main/java/com/ruoyi/http/service/controller/JclyController.java
@@ -41,24 +41,29 @@
                .isNotNull(DeviceLedger::getExternalCode)
                .ne(DeviceLedger::getExternalCode, ""));
        Map<String, String> guidDeviceNameMap = iotDevices.stream()
        Map<String, DeviceLedger> guidDeviceMap = iotDevices.stream()
                .filter(item -> StringUtils.isNotEmpty(item.getExternalCode()))
                .collect(Collectors.toMap(
                        item -> item.getExternalCode().trim(),
                        item -> StringUtils.isNotEmpty(item.getDeviceName()) ? item.getDeviceName().trim() : "",
                        (oldValue, newValue) -> StringUtils.isNotEmpty(oldValue) ? oldValue : newValue,
                        item -> item,
                        (oldValue, newValue) -> StringUtils.isNotEmpty(oldValue.getDeviceName()) ? oldValue : newValue,
                        LinkedHashMap::new
                ));
        List<String> guidList = new ArrayList<>(guidDeviceNameMap.keySet());
        List<String> guidList = new ArrayList<>(guidDeviceMap.keySet());
        List<Map<String, String>> maps = realTimeEnergyConsumptionService
                .getRealData(guidList);
        for (Map<String, String> item : maps) {
            String guid = item.get("guid");
            if (StringUtils.isNotEmpty(guid)) {
                String deviceName = guidDeviceNameMap.get(guid.trim());
                if (StringUtils.isNotEmpty(deviceName)) {
                    item.put("deviceName", deviceName);
                DeviceLedger device = guidDeviceMap.get(guid.trim());
                if (device != null) {
                    if (StringUtils.isNotEmpty(device.getDeviceName())) {
                        item.put("deviceName", device.getDeviceName().trim());
                    }
                    if (StringUtils.isNotEmpty(device.getStorageLocation())) {
                        item.put("storageLocation", device.getStorageLocation().trim());
                    }
                }
            }
        }
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java
@@ -140,7 +140,6 @@
    /**
     * æäº¤
     *
     * @param qualityInspect
     * @return
     */
    @GetMapping("/export/baishi/{id}")
src/main/java/com/ruoyi/quality/utils/QualityInspectTemplateExportHelper.java
@@ -1,7 +1,10 @@
package com.ruoyi.quality.utils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
@@ -22,7 +25,7 @@
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -47,7 +50,54 @@
    }
    public void exportBaiShi(HttpServletResponse response, Long inspectId) {
        export(response, inspectId, BAISHI_TEMPLATE, "百事模版检验结果");
        if (inspectId == null) {
            throw new ServiceException("检验单ID不能为空");
        }
        QualityInspect inspect = qualityInspectMapper.selectById(inspectId);
        if (inspect == null) {
            throw new ServiceException("检验单不存在");
        }
        List<QualityInspectParam> paramList = qualityInspectParamService.list(
                Wrappers.<QualityInspectParam>lambdaQuery()
                        .eq(QualityInspectParam::getInspectId, inspectId)
                        .orderByAsc(QualityInspectParam::getId));
        int index = 1;
        for (QualityInspectParam detail : paramList) {
            detail.setIndex(index);
            index++;
        }
        try (InputStream inputStream = getClass().getResourceAsStream("/static/百事模版.docx")) {
            if (inputStream == null) {
                throw new ServiceException("模板文件不存在:/static/百事模版.docx");
            }
            Configure configure = Configure.builder()
                    .bind("paramList", new HackLoopTableRenderPolicy())
                    .build();
            XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                    new HashMap<String, Object>() {{
                        put("inspect", inspect);
                        put("paramList", paramList);
                    }});
            response.reset();
            response.setContentType("application/msword");
            response.setCharacterEncoding("UTF-8");
            String encodedName = URLEncoder.encode("百事模版检验结果", StandardCharsets.UTF_8).replace("+", "%20");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + encodedName + ".docx");
            try (OutputStream outputStream = response.getOutputStream()) {
                template.write(outputStream);
                outputStream.flush();
            }
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    public void exportDaLi(HttpServletResponse response, Long inspectId) {
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -192,6 +192,7 @@
            data.put("deviceName", device.getDeviceName() != null ? device.getDeviceName() : "");
            data.put("deviceModel", device.getDeviceModel() != null ? device.getDeviceModel() : "");
            data.put("externalCode", device.getExternalCode() != null ? device.getExternalCode() : "");
            data.put("storageLocation", device.getStorageLocation() != null ? device.getStorageLocation() : "");
            Map<String, String> rt = realTimeDataMap.getOrDefault(
                    device.getExternalCode() != null ? device.getExternalCode().trim() : "",
                    Collections.emptyMap());
src/main/resources/static/°ÙÊÂÄ£°æÄ£°å˵Ã÷.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
# ç™¾äº‹æ¨¡ç‰ˆ.docx æ¨¡æ¿åˆ›å»ºè¯´æ˜Ž
## æ–‡ä»¶ä½ç½®
`src/main/resources/static/百事模版.docx`
---
## Word ä¸­åˆ›å»ºæ­¥éª¤
### 1. æ–°å»º Word æ–‡æ¡£
### 2. åˆ›å»ºåŸºæœ¬ä¿¡æ¯è¡¨
插入一个 2列N行的表格:
| é¡¹ç›® | å†…容 |
|------|------|
| äº§å“åç§° | {{inspect.productName}} |
| è§„格型号 | {{inspect.model}} |
| ä¾›åº”商 | {{inspect.supplier}} |
| æ•°é‡ | {{inspect.quantity}} |
| å•位 | {{inspect.unit}} |
| æ£€æµ‹æ—¥æœŸ | {{inspect.checkTime}} |
| æ£€éªŒå‘˜ | {{inspect.checkName}} |
| æ£€æµ‹ç»“æžœ | {{inspect.checkResult}} |
| åˆæ ¼æ•°é‡ | {{inspect.qualifiedQuantity}} |
| ä¸åˆæ ¼æ•°é‡ | {{inspect.unqualifiedQuantity}} |
### 3. åˆ›å»ºæ£€éªŒå‚数列表表(关键!)
插入一个 6列2行的表格:
**表头行:**
| åºå· | æ£€æµ‹æŒ‡æ ‡ | å•位 | æ ‡å‡†å€¼ | å†…控值 | æ£€éªŒå€¼ |
**数据行(循环模板)**,每个单元格输入:
| {{#paramList}}{{index}}{{/paramList}} | {{#paramList}}{{parameterItem}}{{/paramList}} | {{#paramList}}{{unit}}{{/paramList}} | {{#paramList}}{{standardValue}}{{/paramList}} | {{#paramList}}{{controlValue}}{{/paramList}} | {{#paramList}}{{testValue}}{{/paramList}} |
### 4. ä¿å­˜
保存为 `百事模版.docx`,放到 `src/main/resources/static/` ç›®å½•。
---
## ç¤ºä¾‹æ¨¡æ¿å†…容
```
百事检验报告
【基本信息】
产品名称:{{inspect.productName}}
规格型号:{{inspect.model}}
供应商:{{inspect.supplier}}
数量:{{inspect.quantity}} {{inspect.unit}}
检测日期:{{inspect.checkTime}}
检验员:{{inspect.checkName}}
检测结果:{{inspect.checkResult}}
【检验参数】
| åºå· | æ£€æµ‹æŒ‡æ ‡ | å•位 | æ ‡å‡†å€¼ | å†…控值 | æ£€éªŒå€¼ |
| {{#paramList}}{{index}}{{/paramList}} | {{#paramList}}{{parameterItem}}{{/paramList}} | {{#paramList}}{{unit}}{{/paramList}} | {{#paramList}}{{standardValue}}{{/paramList}} | {{#paramList}}{{controlValue}}{{/paramList}} | {{#paramList}}{{testValue}}{{/paramList}} |
合格数量:{{inspect.qualifiedQuantity}}
不合格数量:{{inspect.unqualifiedQuantity}}
```
---
## æ³¨æ„äº‹é¡¹
1. **表格循环必须使用 `{{#paramList}}...{{/paramList}}` åŒ…裹每个字段**
2. è¡¨å¤´è¡Œå’Œæ•°æ®è¡Œéƒ½è¦ä¿ç•™ï¼Œæ•°æ®è¡Œä¼šè‡ªåŠ¨å¤åˆ¶å¤šè¡Œ
3. å ä½ç¬¦è¦å®Œå…¨åŒ¹é…ï¼ŒåŒ…括大小写
4. ä¿å­˜ä¸º `.docx` æ ¼å¼ï¼Œä¸æ˜¯ `.doc`
---
## å¯ç”¨å­—段列表
### inspect(检验单)字段:
- `productName` - äº§å“åç§°
- `model` - è§„格型号
- `supplier` - ä¾›åº”商
- `customer` - å®¢æˆ·åç§°
- `quantity` - æ•°é‡
- `unit` - å•位
- `checkTime` - æ£€æµ‹æ—¥æœŸ
- `checkName` - æ£€éªŒå‘˜
- `checkResult` - æ£€æµ‹ç»“æžœ
- `checkCompany` - æ£€æµ‹å•位
- `qualifiedQuantity` - åˆæ ¼æ•°é‡
- `unqualifiedQuantity` - ä¸åˆæ ¼æ•°é‡
### paramList(检验参数列表)字段:
- `index` - åºå·ï¼ˆè‡ªåŠ¨ç”Ÿæˆï¼‰
- `parameterItem` - æ£€æµ‹æŒ‡æ ‡
- `unit` - å•位
- `standardValue` - æ ‡å‡†å€¼
- `controlValue` - å†…控值
- `testValue` - æ£€éªŒå€¼