下载添加md5sum支持,md5不符合的时候,会重新下载文件。 3027/2 3027/head 3029/target/1 3030/target/1
author孙春阳 <snickersun@163.com>
Tue, 6 Jan 2026 10:33:00 +0000 (18:33 +0800)
committer孙春阳 <snickersun@163.com>
Tue, 6 Jan 2026 10:33:00 +0000 (18:33 +0800)
code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c

index 61d301a..975b9e6 100644 (file)
 #include <dirent.h>
 #include "custom_utils.h"
 #include <sys/time.h>
+#include "mbedtls/md5.h"
+#ifdef SUPPORT_CAT1_SERVICE
+#include "cat1_pub_api.h"
+#endif
 
 /* 宏定义 */
 #define MAX_PATH_LEN 512 // 最大路径长度
@@ -20,6 +24,7 @@ typedef struct
     char *emotion;     // 心情类型(动态分配)
     char *emojiUrl;    // 表情文件URL(动态分配)
     long long size;    // 表情文件大小(字节,支持断点续传判断;0 表示未知)
+    char *md5;         // MD5校验值(动态分配,可为NULL)
 } EmotionEmoji;
 
 /* 表情包配置结构体 */
@@ -40,6 +45,9 @@ static volatile int g_emoji_download_cancelled = 0; /* 取消下载标志 */
 /* 前置声明,便于在本节工具函数中调用 */
 static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji);
 static void ln_gen_emoji_cache_path(const EmojiPackage *pkg, const EmotionEmoji *emoji, char *path, size_t path_len);
+/* MD5计算和校验函数 */
+static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]);
+static int ln_emoji_verify_md5(const char *file_path, const char *expected_md5);
 /* 毫秒时间戳 */
 static inline unsigned long long ln_now_ms(void)
 {
@@ -153,6 +161,11 @@ static void ln_free_emoji_list()
                 free(g_emoji_package.emojis[i].emojiUrl);
                 g_emoji_package.emojis[i].emojiUrl = NULL;
             }
+            if (g_emoji_package.emojis[i].md5)
+            {
+                free(g_emoji_package.emojis[i].md5);
+                g_emoji_package.emojis[i].md5 = NULL;
+            }
         }
         free(g_emoji_package.emojis);
         g_emoji_package.emojis = NULL;
@@ -413,13 +426,103 @@ static void ln_gen_emoji_cache_path(const EmojiPackage *pkg, const EmotionEmoji
     WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 生成表情缓存路径成功,path=%s", path);
 }
 
+/* 计算文件的MD5值(小写十六进制字符串) */
+static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33])
+{
+    if (!file_path || !out_hex) return -1;
+    FILE *fp = fopen(file_path, "rb");
+    if (!fp) return -1;
+    mbedtls_md5_context ctx;
+    mbedtls_md5_init(&ctx);
+    if (mbedtls_md5_starts(&ctx) != 0) { mbedtls_md5_free(&ctx); fclose(fp); return -1; }
+    unsigned char buf[1024];
+    size_t n;
+    while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
+        if (mbedtls_md5_update(&ctx, buf, n) != 0) { mbedtls_md5_free(&ctx); fclose(fp); return -1; }
+    }
+    fclose(fp);
+    unsigned char md5_ctx_buf[16];
+    if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0) { mbedtls_md5_free(&ctx); return -1; }
+    mbedtls_md5_free(&ctx);
+    static const char hex[] = "0123456789abcdef";
+    for (int i = 0; i < 16; i++) {
+        out_hex[i * 2] = hex[(md5_ctx_buf[i] >> 4) & 0x0F];
+        out_hex[i * 2 + 1] = hex[md5_ctx_buf[i] & 0x0F];
+    }
+    out_hex[32] = '\0';
+    return 0;
+}
+
+/* 校验文件的MD5值 */
+static int ln_emoji_verify_md5(const char *file_path, const char *expected_md5)
+{
+    if (file_path == NULL || expected_md5 == NULL || expected_md5[0] == '\0') {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify: invalid args");
+        return -1;
+    }
+    char calc[33] = {0};
+    if (ln_calc_file_md5_lower_hex(file_path, calc) != 0) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify: calc failed for %s", file_path);
+        return -1;
+    }
+    char expect_lower[33] = {0};
+    size_t elen = strlen(expected_md5);
+    size_t copy_len = elen > 32 ? 32 : elen;
+    for (size_t i = 0; i < copy_len; i++) {
+        char ch = expected_md5[i];
+        if (ch >= 'A' && ch <= 'Z') ch = (char)(ch - 'A' + 'a');
+        expect_lower[i] = ch;
+    }
+    expect_lower[32] = '\0';
+    int ok = (strcmp(calc, expect_lower) == 0) ? 1 : 0;
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify result=%s, calc=%s, expect=%s", 
+                  ok ? "OK" : "FAIL", calc, expect_lower);
+    return ok;
+}
+
 /* 工具函数:检查表情是否已缓存 */
 static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji)
 {
     char path[MAX_PATH_LEN] = {0};
     ln_gen_emoji_cache_path(pkg, emoji, path, sizeof(path));
     struct stat st;
-    return (stat(path, &st) == 0 && S_ISREG(st.st_mode)) ? 1 : 0;
+    
+    /* 检查文件是否存在,同时获取文件大小(stat是元数据操作,很快) */
+    if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
+        return 0;
+    }
+    
+    /* 优先检查文件大小(使用已获取的st.st_size,避免额外IO)
+     * 如果大小不匹配,直接返回,避免读取整个文件计算MD5 */
+    if (emoji->size > 0) {
+        unsigned long long file_size = (unsigned long long)st.st_size;
+        if (file_size != (unsigned long long)emoji->size) {
+            /* 文件大小不匹配,可能不完整,删除文件并返回0以触发重新下载 */
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 文件大小不匹配(期望:%lld, 实际:%llu),删除不完整文件: %s", 
+                          emoji->emotion ? emoji->emotion : "unknown", emoji->size, file_size, path);
+            unlink(path);
+            return 0;
+        }
+    }
+    
+    /* 文件大小匹配后,再进行MD5校验(需要读取整个文件) */
+    if (emoji->md5 != NULL && emoji->md5[0] != '\0') {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 检查已缓存文件 %s 的MD5,期望MD5=%s", 
+                      emoji->emotion ? emoji->emotion : "unknown", emoji->md5);
+        int md5_ok = ln_emoji_verify_md5(path, emoji->md5);
+        if (md5_ok != 1) {
+            /* MD5校验失败,文件可能不完整,删除文件并返回0以触发重新下载 */
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败,删除不完整文件: %s", 
+                          emoji->emotion ? emoji->emotion : "unknown", path);
+            unlink(path);
+            return 0;
+        } else {
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 已缓存文件MD5校验通过", 
+                          emoji->emotion ? emoji->emotion : "unknown");
+        }
+    }
+    
+    return 1;
 }
 
 /* 批量下载表情并上报结果 */
@@ -492,12 +595,16 @@ static void ln_emoji_download(void *arg)
         }
     }
     downloaded_count = cached_count;
-    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d", downloaded_count);
+    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d/%d", downloaded_count, pkg->emojiCount);
     if (pkg->emojiCount > 0)
     {
         int init_progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100);
+        // 确保进度在0-100范围内
+        if (init_progress < 0) init_progress = 0;
+        if (init_progress > 100) init_progress = 100;
         if (init_progress > 0)
         {
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始进度=%d%%", init_progress);
             push_download_progress_notification(init_progress);
         }
     }
@@ -547,33 +654,108 @@ static void ln_emoji_download(void *arg)
             break;
         }
 
-        if (uapi_ln_download_file_resume_with_retry(pkg->emojis[i].emojiUrl, save_path, 10) != ERRCODE_SUCC)
+        int download_success = 0;
+        int max_retry = 2; // 最大重试次数(包括MD5校验失败后的重试),减少重试次数以加快失败处理
+        int retry_count = 0;
+        
+        while (retry_count < max_retry)
         {
-            // 检查是否因为取消而失败
-            if (g_emoji_download_cancelled)
+            // 如果是重试,先删除旧文件
+            if (retry_count > 0)
             {
-                WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载被取消");
-                all_success = 0;
-                break;
+                unlink(save_path);
+                WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 第%d次重试下载", pkg->emojis[i].emotion, retry_count);
             }
+            
+            // 减少内部重试次数,从10次减少到3次,避免单个文件耗时过长
+            if (uapi_ln_download_file_resume_with_retry(pkg->emojis[i].emojiUrl, save_path, 3) != ERRCODE_SUCC)
+            {
+                // 检查是否因为取消而失败
+                if (g_emoji_download_cancelled)
+                {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载被取消");
+                    all_success = 0;
+                    break;
+                }
 
-            all_success = 0;
-            failed_count++;
-            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载失败", pkg->emojis[i].emotion);
-            // 上报下载失败状态
-            char fail_msg[128];
-            snprintf(fail_msg, sizeof(fail_msg), "表情 %s 下载失败", pkg->emojis[i].emotion);
-            ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg);
-            /* 单个文件三次重试仍失败,终止后续下载并尽快上报失败 */
+                retry_count++;
+                if (retry_count >= max_retry)
+                {
+                    // 单个文件重试次数已用完,立即跳过该文件,继续下载下一个文件
+                    all_success = 0;
+                    failed_count++;
+                    // 删除可能存在的部分下载文件,避免留下不完整的文件
+                    unlink(save_path);
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载失败(已重试%d次),删除不完整文件,跳过该文件,继续下载下一个", pkg->emojis[i].emotion, retry_count);
+                    // 上报下载失败状态
+                    char fail_msg[128];
+                    snprintf(fail_msg, sizeof(fail_msg), "表情 %s 下载失败", pkg->emojis[i].emotion);
+                    ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg);
+                    // 跳过该文件,继续下载下一个文件(不终止整个下载任务)
+                    break; // 退出while循环,继续for循环的下一个文件
+                }
+                // 重试前等待一段时间,避免频繁重试
+                sleep(2);
+                continue; // 继续重试
+            }
+            
+            // 下载成功,进行MD5校验(如果有MD5值)
+            if (pkg->emojis[i].md5 && pkg->emojis[i].md5[0] != '\0')
+            {
+                WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成,开始MD5校验,期望MD5=%s", 
+                              pkg->emojis[i].emotion, pkg->emojis[i].md5);
+                
+                if (ln_emoji_verify_md5(save_path, pkg->emojis[i].md5) != 1)
+                {
+                    // MD5校验失败,删除文件并重试
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败,删除文件并重试", pkg->emojis[i].emotion);
+                    unlink(save_path);
+                    retry_count++;
+                    if (retry_count >= max_retry)
+                    {
+                        // MD5校验失败且重试次数已用完,立即跳过该文件,继续下载下一个文件
+                        all_success = 0;
+                        failed_count++;
+                        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败(已重试%d次),跳过该文件,继续下载下一个", pkg->emojis[i].emotion, retry_count);
+                        char fail_msg[128];
+                        snprintf(fail_msg, sizeof(fail_msg), "表情 %s MD5校验失败", pkg->emojis[i].emotion);
+                        ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg);
+                        break; // 退出while循环,继续for循环的下一个文件
+                    }
+                    // 重试前等待一段时间,避免频繁重试
+                    sleep(2);
+                    continue; // 继续重试
+                }
+                else
+                {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验通过", pkg->emojis[i].emotion);
+                }
+            }
+            else
+            {
+                WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成(无MD5校验值,跳过校验)", pkg->emojis[i].emotion);
+            }
+            
+            // 下载和校验都成功
+            download_success = 1;
             break;
         }
-        else
+        
+        // 处理下载结果(成功或失败)
+        if (download_success)
         {
             downloaded_count++;
             /* 表情进度上报,按下载成功表情数量计算 */
             int progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100);
+            // 确保进度在0-100范围内
+            if (progress < 0) progress = 0;
+            if (progress > 100) progress = 100;
+            
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载并校验完成, downloaded_count=%d/%d, progress=%d%%", 
+                          pkg->emojis[i].emotion, downloaded_count, pkg->emojiCount, progress);
+            
+            // 更新UI进度
             push_download_progress_notification(progress);
-            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成", pkg->emojis[i].emotion);
             
             // 每次进度更新都同步发送协议给服务器更新进度
             char progress_msg[64];
@@ -769,7 +951,11 @@ void ln_start_emoji_download(void)
     // 反初始化HTTP文件下载服务
     http_file_download_deinit();
 }
-
+/**
+ * 带md5sum的参考格式
+ * 解析时要注意带md5和不带md5的要兼容。
+ * {"deviceId":"867998086628226","timestamp":1767672001,"messageId":"18692b900b384a40","command":"DownloadEmoji","data":{"characterId":"de50ca21420f","eyesColor":"brown","eyesShape":"calm-eyes","emotionEmojis":[{"emotion":"happy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Smile.mp4","size":715594,"md5":"262d7d33f273c0aafa137e2d72d2eaf9"},{"emotion":"laughing","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Laugh.mp4","size":773624,"md5":"7d74df7f75661452ce3d1e51e2bd3d58"},{"emotion":"angry","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Angry.mp4","size":754492,"md5":"b4e6f8ac6c0d20c1e71336ca10d82ffa"},{"emotion":"wronged","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Wronged.mp4","size":1169130,"md5":"62f651821a12b224bd0b9e4d61ad16d8"},{"emotion":"heartbeat","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Heartbeat.mp4","size":940392,"md5":"2e4df95c116e82f4ab31ed21b6d7fc49"},{"emotion":"awkward","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Embarrassed.mp4","size":793970,"md5":"e168cc2e789732c61653c91cb72a3a27"},{"emotion":"confused","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Doubt.mp4","size":812766,"md5":"70c2c027e261394c78dbaaff5284829c"},{"emotion":"shocked","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Shocked.mp4","size":843018,"md5":"decc6a03f7d1356a6f3f82f57636bf0a"},{"emotion":"crying","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Tears.mp4","size":898970,"md5":"369e17f42842db9c0b1a330800416760"},{"emotion":"blowing-kiss","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Kiss.mp4","size":539583,"md5":"d1dc573d076a59ffae8fbdbb9eceb432"},{"emotion":"sleepy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Sleepy.mp4","size":661627,"md5":"2483cc4a1aa57b9b15b8ab3d17bf453b"},{"emotion":"mischievous","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Naughty.mp4","size":636318,"md5":"efe8d0d6d10bebf32cfe10689cba8702"},{"emotion":"speechless","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Speechless.mp4","size":875623,"md5":"a58d5ba63375732a8259f294dfe35a92"},{"emotion":"dizzy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Dizzy.mp4","size":1133467,"md5":"7f6988e7509655d27ac4a0e53859930b"},{"emotion":"thinking","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Think.mp4","size":960243,"md5":"b09dc26e2a9b1ece3609bb5c019c5619"},{"emotion":"working","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/WorkAndStudy.mp4","size":1029099,"md5":"bc483c6728b3767cc1bd5dff7a1a2411"},{"emotion":"exercising","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Exercise.mp4","size":1056919,"md5":"65382bf4fc5a357dcc963d4dc4495ec8"},{"emotion":"playing","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Play.mp4","size":1077517,"md5":"d1a379980e2c144b02c915b41711e62f"},{"emotion":"daydreaming","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Daze.mp4","size":1034574,"md5":"f71e5d96ba3afff009ff70ef0f39ebe8"}],"size":16706926}}
+ */
 /* 解析下载表情命令JSON */
 void ln_parse_download_emoji(const char *json_buf, int json_len)
 {
@@ -878,9 +1064,11 @@ void ln_parse_download_emoji(const char *json_buf, int json_len)
                         cJSON *emotion = cJSON_GetObjectItem(item, "emotion");
                         cJSON *emojiUrl = cJSON_GetObjectItem(item, "emojiUrl");
                         cJSON *size_node = cJSON_GetObjectItem(item, "size");
+                        cJSON *md5_node = cJSON_GetObjectItem(item, "md5");
 
                         g_emoji_package.emojis[i].emotion = ln_safe_strdup((emotion && cJSON_IsString(emotion)) ? emotion->valuestring : "");
                         g_emoji_package.emojis[i].emojiUrl = ln_safe_strdup((emojiUrl && cJSON_IsString(emojiUrl)) ? emojiUrl->valuestring : "");
+                        
                         /* 解析单个表情大小(字节) */
                         long long emoji_size = 0;
                         if (size_node)
@@ -895,11 +1083,24 @@ void ln_parse_download_emoji(const char *json_buf, int json_len)
                             }
                         }
                         g_emoji_package.emojis[i].size = (emoji_size > 0) ? emoji_size : 0;
-                        WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, "  emoji %d: emotion=%s, url=%s, size=%lld",
+                        
+                        /* 解析MD5校验值 */
+                        if (md5_node && cJSON_IsString(md5_node) && md5_node->valuestring)
+                        {
+                            g_emoji_package.emojis[i].md5 = ln_safe_strdup(md5_node->valuestring);
+                        }
+                        else
+                        {
+                            g_emoji_package.emojis[i].md5 = NULL;
+                        }
+                        
+                        /* 打印解析到的size和md5信息 */
+                        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "  emoji %d: emotion=%s, url=%s, size=%lld, md5=%s",
                                       i + 1,
                                       g_emoji_package.emojis[i].emotion,
                                       g_emoji_package.emojis[i].emojiUrl,
-                                      g_emoji_package.emojis[i].size);
+                                      g_emoji_package.emojis[i].size,
+                                      g_emoji_package.emojis[i].md5 ? g_emoji_package.emojis[i].md5 : "NULL");
                     }
                 }
             }