根據錯誤日誌,問題出現在執行MyBatis的update操作時,參數映射失敗。具體來説,在更新CsMessage時,MyBatis在動態SQL中找不到名為'sessionId'的參數。可用的參數是[param1, et]。

查看EnhancedMessageService中的deliverMessageToCustomer方法,其中有一行:
csMessageMapper.updateById(csMessage);

這裏我們使用MyBatis-Plus的updateById方法,它期望傳入一個實體對象。但是,從錯誤信息來看,在動態SQL中,參數被命名為'et'(這是MyBatis-Plus的默認命名,代表實體對象)和'param1'(代表第一個參數,也就是實體對象)。但是在XML中,我們使用了條件判斷,如:
<if test="sessionId != null and sessionId != ''">session_id = #{sessionId},</if>

這裏直接使用了屬性名'sessionId',而MyBatis在解析時會在傳入的參數中查找名為'sessionId'的屬性。由於我們傳入的是CsMessage對象,它確實有sessionId屬性,但是為什麼找不到呢?

可能的原因是在MyBatis的配置中,參數映射的方式有問題。但是,從錯誤信息來看,可用的參數是[param1, et],這意味着MyBatis將參數命名為param1,同時還有一個別名et(這是MyBatis-Plus的默認行為)。在XML中,我們直接使用屬性名,MyBatis會從實體對象中獲取屬性,但是這裏卻報錯説找不到'sessionId'參數。

另一種可能是,在XML中,我們使用了多個updateById的映射,而MyBatis在解析時出現了混淆。查看CsMessageMapper.xml,我們發現有兩個updateById的映射,這可能會導致MyBatis使用第一個映射,而第一個映射的參數配置可能不正確。

在CsMessageMapper.xml中,有兩個updateById的映射,這是不允許的。MyBatis會根據方法名和參數類型來匹配映射,但是同一個命名空間下不能有相同的id。因此,我們需要刪除重複的updateById映射。

解決方案:

  1. 刪除CsMessageMapper.xml中重複的updateById映射,只保留一個。
  2. 確保在updateById的映射中,使用正確的參數類型,並且使用et作為別名來引用實體屬性(這是MyBatis-Plus的默認行為),或者直接使用屬性名(因為MyBatis-Plus默認會將實體對象作為參數,並且屬性可以直接訪問)。

但是,我們注意到在XML中,我們使用的是:
<if test="sessionId != null and sessionId != ''">session_id = #{sessionId},</if>

這裏直接使用sessionId,而不是et.sessionId。在MyBatis-Plus中,默認的參數名是et,所以我們應該使用et.sessionId?實際上,MyBatis-Plus在更新時,會使用實體對象作為參數,並且默認的別名是et。但是,在動態SQL中,我們也可以直接使用屬性名,因為MyBatis會從實體對象中查找屬性。但是,如果同時有多個參數,就必須使用別名。

由於我們只有一個參數(即實體對象),所以可以直接使用屬性名。但是,從錯誤信息來看,MyBatis在參數映射時,可用的參數是[param1, et],這意味着我們有兩個參數?實際上,MyBatis-Plus的updateById方法只有一個參數,但是MyBatis-Plus在內部處理時,可能會將實體對象作為et和param1兩個別名。所以,我們可以通過et.sessionId來訪問,也可以直接使用sessionId。

但是,這裏報錯説找不到'sessionId',説明MyBatis在參數中沒有找到名為sessionId的參數。因此,我們可以嘗試修改XML,使用et.sessionId。

但是,我們也可以看到,在CsMessageMapper.xml中,有兩個updateById的映射,這可能是導致問題的根本原因。因為MyBatis在解析映射時,可能會使用第一個映射,而第一個映射可能沒有正確配置參數。

因此,我們首先刪除重複的updateById映射。

步驟:

  1. 打開CsMessageMapper.xml,刪除重複的updateById映射,只保留一個。
  2. 確保保留的updateById映射中,參數類型為CsMessage,並且使用正確的動態SQL。

另外,我們也可以考慮使用MyBatis-Plus的默認實現,而不需要自己寫updateById的映射。因為MyBatis-Plus已經提供了通用的updateById方法,我們不需要在XML中編寫。所以,我們可以將CsMessageMapper.xml中的updateById映射刪除,直接使用MyBatis-Plus的通用方法。

但是,我們注意到在CsMessageMapper.java中,我們繼承了BaseMapper,它已經包含了updateById方法。因此,我們不需要在XML中編寫updateById映射。所以,我們可以將CsMessageMapper.xml中的兩個updateById映射全部刪除。

然後,我們檢查CsMessageMapper.xml中是否還有其他重複的映射,確保每個映射的id都是唯一的。

另外,我們還可以在EnhancedMessageService中,使用MyBatis-Plus的updateById方法時,確保傳入的csMessage對象中的messageId不為空,因為它是主鍵。

我們修改CsMessageMapper.xml,刪除重複的updateById映射,只保留其他自定義的映射。

修改後的CsMessageMapper.xml應該只包含一個updateById映射(如果確實需要自定義,否則刪除)?但是,我們注意到在CsMessageMapper.xml中,有兩個updateById映射,我們將其全部刪除,因為MyBatis-Plus已經提供了實現。

但是,我們還有一個自定義的update方法:updateCsMessage,它也是更新操作,但是根據messageId更新。我們可能在其他地方使用了updateCsMessage,所以保留updateCsMessage。

所以,我們刪除CsMessageMapper.xml中兩個updateById映射,然後測試。

如果問題仍然存在,我們可以在CsMessageMapper.java中,使用@Param註解來明確參數名,或者在XML中使用_parameter(代表整個參數)來訪問屬性。但是,由於我們使用的是MyBatis-Plus,它已經幫我們處理了參數別名,所以刪除重複映射後應該可以解決問題。

我們修改CsMessageMapper.xml,刪除兩個updateById映射,然後重新部署測試。

如果問題仍然存在,我們可以嘗試在XML中使用et.sessionId,例如:
<if test="et.sessionId != null and et.sessionId != ''">session_id = #{et.sessionId},</if>

但是,由於我們刪除了自定義的updateById映射,那麼就會使用MyBatis-Plus自帶的updateById方法,它的XML映射是在MyBatis-Plus的jar包中定義的,我們不應該修改。所以,我們不應該在CsMessageMapper.xml中定義updateById。

因此,我們刪除CsMessageMapper.xml中重複的updateById映射,然後清理緩存,重新啓動應用。

如果問題仍然存在,我們再考慮其他方案。

現在,我們修改CsMessageMapper.xml,刪除兩個updateById映射。

修改後的CsMessageMapper.xml中,不再有updateById映射,而是使用MyBatis-Plus自帶的updateById方法。

我們重新部署應用,觀察錯誤是否解決。

如果錯誤仍然存在,那麼可能是MyBatis-Plus的版本問題,或者配置問題。我們可以嘗試在application.yml中配置mybatis-plus的configuration,設置use-actual-param-name為true,例如:

mybatis-plus:
configuration:
use-actual-param-name: true

但是,我們也可以考慮在EnhancedMessageService中,使用我們自定義的update方法,即使用updateDeliveredStatus方法,只更新送達狀態,而不是整個實體。這樣可能更安全。

在EnhancedMessageService的deliverMessageToCustomer方法中,我們更新csMessage的delivered字段,然後調用updateById。實際上,我們也可以只更新delivered字段,使用:
csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), true);

這樣,我們使用自定義的updateDeliveredStatus方法,它只更新delivered字段,避免動態SQL中的複雜判斷。

所以,我們可以修改deliverMessageToCustomer方法,將:
csMessage.setDelivered(true);
csMessageMapper.updateById(csMessage);

改為:
csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), true);

同樣,在else分支中,我們也使用updateDeliveredStatus方法。

這樣修改可以避免使用動態SQL,從而避免參數映射問題。

我們選擇這種方案,因為更安全,並且效率更高(只更新一個字段)。

修改EnhancedMessageService中的deliverMessageToCustomer方法:

原始代碼:
if (isOnline) {
try {
...
csMessage.setDelivered(true);
csMessageMapper.updateById(csMessage);
} catch (Exception e) {
...
csMessage.setDelivered(false);
csMessageMapper.updateById(csMessage);
}
} else {
...
csMessage.setDelivered(false);
csMessageMapper.updateById(csMessage);
}

修改為:
if (isOnline) {
try {
...
csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), true);
} catch (Exception e) {
...
csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), false);
}
} else {
...
csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), false);
}

注意:我們不再需要設置csMessage的delivered屬性,因為直接調用updateDeliveredStatus方法更新數據庫。但是,csMessage對象在內存中的delivered屬性並沒有更新,這可能會影響後續使用。但是,在這個方法中,csMessage對象之後並沒有被使用,所以可以這樣修改。

我們進行修改,然後測試。

同時,我們也要修改handleAgentReply方法中保存客服消息後的更新操作,將updateById改為updateDeliveredStatus嗎?不,在handleAgentReply方法中,我們保存客服消息後,在deliverMessageToCustomer方法中會更新送達狀態。所以,我們只需要修改deliverMessageToCustomer方法中的更新操作。

我們修改EnhancedMessageService中的deliverMessageToCustomer方法,將所有的updateById調用改為updateDeliveredStatus。

修改後,我們重新部署應用,觀察錯誤是否解決。

如果問題解決,那麼我們就避免了動態SQL的參數映射問題。

如果問題仍然存在,那麼可能是其他原因,我們需要進一步分析。

現在,我們按照上述方案修改代碼。

根據錯誤日誌,問題出現在執行 MyBatis 的 updateById 方法時,參數映射失敗。具體錯誤是:

Parameter 'sessionId' not found. Available parameters are [param1, et]

這個問題是由於 MyBatis-Plus 的 XML 映射文件中存在重複的 updateById 定義,導致參數映射混亂。

問題分析

查看 CsMessageMapper.xml 文件,確實存在兩個相同的 updateById 映射:

<update id="updateById" parameterType="com.storm.customerService.domain.CsMessage">
    <!-- 第一個定義 -->
</update>

<update id="updateById" parameterType="com.storm.customerService.domain.CsMessage">
    <!-- 第二個定義 -->
</update>

解決方案

方案1:修復 XML 映射文件(推薦)

刪除 CsMessageMapper.xml 中重複的 updateById 定義,只保留一個:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.storm.customerService.mapper.CsMessageMapper">

    <resultMap type="com.storm.customerService.domain.CsMessage" id="CsMessageResult">
        <result property="messageId"    column="message_id"    />
        <result property="sessionId"    column="session_id"    />
        <result property="senderType"    column="sender_type"    />
        <result property="senderId"    column="sender_id"    />
        <result property="senderName"    column="sender_name"    />
        <result property="content"    column="content"    />
        <result property="messageType"    column="message_type"    />
        <result property="sendTime"    column="send_time"    />
        <result property="delivered"    column="delivered"    />
    </resultMap>

    <sql id="selectCsMessageVo">
        select message_id, session_id, sender_type, sender_id, sender_name, content, message_type, send_time, delivered from cs_message
    </sql>

    <!-- 刪除重複的 updateById 定義,只保留一個 -->
    <update id="updateById" parameterType="com.storm.customerService.domain.CsMessage">
        UPDATE cs_message
        <set>
            <if test="sessionId != null and sessionId != ''">session_id = #{sessionId},</if>
            <if test="senderType != null and senderType != ''">sender_type = #{senderType},</if>
            <if test="senderId != null and senderId != ''">sender_id = #{senderId},</if>
            <if test="senderName != null and senderName != ''">sender_name = #{senderName},</if>
            <if test="content != null and content != ''">content = #{content},</if>
            <if test="messageType != null">message_type = #{messageType},</if>
            <if test="sendTime != null">send_time = #{sendTime},</if>
            <if test="delivered != null">delivered = #{delivered},</if>
        </set>
        WHERE message_id = #{messageId}
    </update>

    <!-- 其他映射保持不變 -->
    <!-- ... -->
</mapper>

方案2:使用註解方法替代 XML 映射

在 EnhancedMessageService.java 中,將 updateById 調用改為使用註解方法:

/**
 * 改進的消息投遞方法 - 修復版本
 */
private void deliverMessageToCustomer(String customerId, String agentName,
                                      String content, CsMessage csMessage) {
    // 使用改進的在線狀態檢查
    boolean isOnline = isCustomerReallyOnline(customerId);

    log.info("消息投遞檢查 - 客户: {}, 在線: {}, WebSocket狀態: {}, 會話存在: {}",
            customerId, isOnline, customerOnlineStatus.get(customerId),
            customerSessionMap.containsKey(customerId));

    if (isOnline) {
        try {
            WebSocketSession session = customerSessionMap.get(customerId);
            if (session != null && session.isOpen()) {
                // 構建消息格式
                Map<String, Object> messageData = new HashMap<>();
                messageData.put("type", "AGENT_MESSAGE");
                messageData.put("messageId", csMessage.getMessageId());
                messageData.put("senderName", agentName);
                messageData.put("content", content);
                messageData.put("timestamp", System.currentTimeMillis());
                messageData.put("agentName", agentName);

                String jsonMessage = objectMapper.writeValueAsString(messageData);

                // 通過WebSocket推送到客户APP
                webSocketHandler.sendMessageToCustomer(customerId, jsonMessage, csMessage.getMessageId());

                // 使用註解方法更新送達狀態
                csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), true);
                log.info("客服消息已推送到客户: {} - {}", customerId, content);
            } else {
                log.warn("WebSocket會話無效,消息暫存: {}", customerId);
                csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), false);
            }
        } catch (Exception e) {
            log.error("推送消息到客户失敗: {}", customerId, e);
            // 推送失敗時更新狀態
            updateCustomerOnlineStatus(customerId, false);
            csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), false);
        }
    } else {
        log.warn("客户不在線,消息暫存: {}", customerId);
        // 消息保持未送達狀態,等客户上線後重新發送
        csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), false);
    }
}

方案3:簡化 XML 映射

如果確實需要保留 XML 映射,可以簡化 updateById 方法:

<update id="updateById" parameterType="com.storm.customerService.domain.CsMessage">
    UPDATE cs_message 
    SET 
        session_id = #{sessionId},
        sender_type = #{senderType},
        sender_id = #{senderId},
        sender_name = #{senderName},
        content = #{content},
        message_type = #{messageType},
        send_time = #{sendTime},
        delivered = #{delivered}
    WHERE message_id = #{messageId}
</update>

推薦實施步驟

  1. 立即修復:採用方案2,使用註解方法 updateDeliveredStatus 替代 updateById
  2. 長期修復:清理 XML 映射文件中的重複定義(方案1)
  3. 測試驗證:確保客服回覆功能正常工作

修改後的關鍵代碼

在 EnhancedMessageService.java 中替換所有有問題的 updateById 調用:

// 將原來的:
csMessage.setDelivered(true);
csMessageMapper.updateById(csMessage);

// 改為:
csMessageMapper.updateDeliveredStatus(csMessage.getMessageId(), true);

這樣修改後,將避免 MyBatis 參數映射的問題,確保客服回覆功能正常運行。