feat(database): 添加附件数据迁移脚本从common_file到storage_blob_storage_attachment

- 创建数据库迁移脚本实现从旧表common_file到新表storage_blob和storage_attachment的数据迁移
- 实现文件数据迁移包括资源键、内容类型、原始文件名、唯一文件名、文件大小和路径字段映射
- 实现附件关联数据迁移包括业务类型映射、业务ID关联和存储blob关联
- 添加Linux服务器文件大小更新脚本用于获取实际文件字节大小
- 提供数据验证查询确保迁移前后数据一致性
- 包含按类型统计迁移数量的验证功能
- 添加数据校验步骤检查空值和异常数据
- 提供迁移完成后的数据清理建议和备份方案
已添加3个文件
364 ■■■■■ 文件已修改
doc/20260604_common_file迁移到storage_blob_storage_attachment.sql 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/update_blob_file.sh 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/update_file_size.sh 49 ●●●●● 补丁 | 查看 | 原始文档 | 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/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"