最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++微服務(wù)UserServer設(shè)計(jì)與實(shí)現(xiàn)方法詳解

 更新時(shí)間:2025年11月21日 09:49:59   作者:會(huì)開(kāi)花的二叉樹(shù)  
在當(dāng)今云原生時(shí)代,微服務(wù)架構(gòu)已成為構(gòu)建大規(guī)模分布式系統(tǒng)的主流選擇,下面這篇文章主要介紹了C++微服務(wù)UserServer設(shè)計(jì)與實(shí)現(xiàn)的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下

前言

做 IM 項(xiàng)目時(shí),用戶服務(wù)(UserServer)是整個(gè)系統(tǒng)的基石 —— 所有業(yè)務(wù)(好友、消息、朋友圈)都依賴用戶認(rèn)證和基礎(chǔ)信息。這篇文章就從實(shí)戰(zhàn)角度,聊聊我是怎么設(shè)計(jì)、實(shí)現(xiàn) UserServer 的,包括核心功能落地、依賴替換(比如用模擬短信服務(wù)替代真實(shí)平臺(tái))、以及那些踩過(guò)的坑,希望能給做 C++ 后端的朋友一些參考。

一、先搞懂:UserServer 在 IM 系統(tǒng)里的角色

在之前的 IM 微服務(wù)架構(gòu)里,UserServer 承擔(dān) 3 個(gè)核心職責(zé):

  1. 用戶認(rèn)證:注冊(cè)(用戶名 / 手機(jī)號(hào))、登錄(用戶名密碼 / 手機(jī)驗(yàn)證碼)、會(huì)話管理;

  2. 用戶信息管理:頭像、昵稱、簽名、手機(jī)號(hào)的修改與查詢;

  3. 基礎(chǔ)支撐:給其他服務(wù)提供用戶信息(比如好友服務(wù)查好友資料、消息服務(wù)查發(fā)送者信息)。

所以設(shè)計(jì)時(shí),必須考慮可擴(kuò)展性(比如后續(xù)加第三方登錄)、可測(cè)試性(比如不用真實(shí)短信也能測(cè)手機(jī)號(hào)登錄)、性能(登錄會(huì)話用 Redis 緩存,避免查庫(kù))。

二、核心設(shè)計(jì):從依賴到架構(gòu),拒絕 “硬編碼”

1. 依賴注入:讓服務(wù)更靈活(踩過(guò)坑才懂的重要性)

最開(kāi)始寫(xiě) UserServiceImpl 的時(shí)候,我直接在類里 new 了 DMSClient(真實(shí)短信服務(wù)),后來(lái)發(fā)現(xiàn)個(gè)人開(kāi)發(fā)者沒(méi)法申請(qǐng)企業(yè)短信資質(zhì),想換成模擬服務(wù)時(shí),改了大半天代碼。后來(lái)重構(gòu)時(shí),把所有外部依賴都通過(guò)構(gòu)造函數(shù)注入,這才清爽了。

看核心構(gòu)造函數(shù):

UserServiceImpl(
    const MockSmsClient::ptr& mock_sms_client,  // 短信服務(wù)(真實(shí)/模擬可替換)
    const std::shared_ptr<elasticlient::Client>& es_client,  // ES(用戶搜索用)
    const std::shared_ptr<odb::core::database>& mysql_client,  // MySQL(用戶數(shù)據(jù)存儲(chǔ))
    const std::shared_ptr<sw::redis::Redis>& redis_client,  // Redis(會(huì)話/驗(yàn)證碼)
    const ServiceManager::ptr& channel_manager,  // 服務(wù)管理(調(diào)用文件服務(wù)等)
    const std::string& file_service_name  // 文件服務(wù)名稱(定位服務(wù)用)
) :
    _es_user(std::make_shared<ESUser>(es_client)),
    _mysql_user(std::make_shared<UserTable>(mysql_client)),
    _redis_session(std::make_shared<Session>(redis_client)),
    _redis_status(std::make_shared<Status>(redis_client)),
    _redis_codes(std::make_shared<Codes>(redis_client)),
    _file_service_name(file_service_name),
    _mm_channels(channel_manager),
    _dms_client(mock_sms_client)  // 注入短信服務(wù),而非內(nèi)部new
{
    _es_user->createIndex();  // 初始化ES用戶索引
}

這樣設(shè)計(jì)的好處:

  • 替換依賴不碰業(yè)務(wù)代碼:把MockSmsClient換成真實(shí)DMSClient,只需要改構(gòu)建器(UserServerBuilder)的初始化邏輯,UserServiceImpl里的GetPhoneVerifyCode完全不用動(dòng);

  • 測(cè)試方便:寫(xiě)單元測(cè)試時(shí),能注入 “假的 Redis 客戶端”“假的 ES 客戶端”,不用依賴真實(shí)中間件。

2. 核心依賴拆解:每個(gè)組件各司其職

依賴組件作用實(shí)戰(zhàn)細(xì)節(jié)
MockSmsClient模擬短信發(fā)送內(nèi)部不調(diào)用外部平臺(tái),只打印日志 + 存 Redis 驗(yàn)證碼
ODB(MySQL ORM)用戶數(shù)據(jù) CRUD需用odb工具生成 ORM 代碼,避免手寫(xiě) SQL
Redis(sw::redis++)會(huì)話存儲(chǔ)、驗(yàn)證碼、登錄狀態(tài)會(huì)話過(guò)期設(shè) 2 小時(shí),驗(yàn)證碼 5 分鐘過(guò)期
Elasticlient用戶搜索(比如好友搜索)初始化時(shí)創(chuàng)建user索引,支持昵稱 / 手機(jī)號(hào)模糊查
ServiceManager調(diào)用其他微服務(wù)(如文件服務(wù)上傳頭像)基于 Etcd 發(fā)現(xiàn)服務(wù)節(jié)點(diǎn),RR 輪詢負(fù)載均衡

三、核心功能落地:從代碼到業(yè)務(wù),講透細(xì)節(jié)

1. 用戶注冊(cè):不只是存數(shù)據(jù),還要做校驗(yàn)

注冊(cè)邏輯看起來(lái)簡(jiǎn)單,但細(xì)節(jié)容易出問(wèn)題,比如密碼強(qiáng)度、昵稱重復(fù)??搓P(guān)鍵代碼:

bool password_check(const std::string &password) {
    // 密碼規(guī)則:6-15位,只含字母、數(shù)字、_、-
    if (password.size() < 6 || password.size() > 15) {
        LOG_ERROR("密碼長(zhǎng)度不合法:{}-{}", password, password.size());
        return false;
    }
    for (int i = 0; i < password.size(); i++) {
        if (!((password[i] > 'a' && password[i] < 'z') ||
            (password[i] > 'A' && password[i] < 'Z') ||
            (password[i] > '0' && password[i] < '9') ||
            password[i] == '_' || password[i] == '-')) {
            LOG_ERROR("密碼字符不合法:{}", password);
            return false;
        }
    }
    return true;
}

void UserRegister(...) {
    // 1. 取請(qǐng)求參數(shù)
    std::string nickname = request->nickname();
    std::string password = request->password();
    
    // 2. 校驗(yàn)昵稱、密碼
    if (!nickname_check(nickname)) {
        return err_response("用戶名長(zhǎng)度不合法!");
    }
    if (!password_check(password)) {
        return err_response("密碼格式不合法!");
    }
    
    // 3. 查昵稱是否已存在(ODB ORM調(diào)用)
    auto user = _mysql_user->select_by_nickname(nickname);
    if (user) {
        return err_response("用戶名被占用!");
    }
    
    // 4. 生成用戶ID,存MySQL+ES
    std::string uid = uuid();  // 自定義工具函數(shù),生成唯一ID
    user = std::make_shared<User>(uid, nickname, password);
    if (!_mysql_user->insert(user)) {
        return err_response("Mysql數(shù)據(jù)庫(kù)新增數(shù)據(jù)失敗!");
    }
    if (!_es_user->appendData(uid, "", nickname, "", "")) {
        return err_response("ES搜索引擎新增數(shù)據(jù)失敗!");
    }
    
    // 5. 返回成功
    response->set_success(true);
}

實(shí)戰(zhàn)踩坑:最開(kāi)始沒(méi)做密碼校驗(yàn),測(cè)試時(shí)輸入特殊字符導(dǎo)致數(shù)據(jù)庫(kù)存儲(chǔ)異常,后來(lái)加了嚴(yán)格的字符校驗(yàn),還在日志里打印不合法的密碼,方便排查問(wèn)題。

2. 手機(jī)號(hào)驗(yàn)證碼:用模擬服務(wù)突破平臺(tái)限制

真實(shí)短信服務(wù)(如阿里云 DMS)需要企業(yè)資質(zhì),個(gè)人開(kāi)發(fā)沒(méi)法用,所以做了MockSmsClient來(lái)模擬。核心邏輯是:生成驗(yàn)證碼→存 Redis→返回驗(yàn)證碼 ID,校驗(yàn)時(shí)從 Redis 查。

第一步:設(shè)計(jì) MockSmsClient

// mock_sms.hpp
class MockSmsClient {
public:
    using ptr = std::shared_ptr<MockSmsClient>;
    MockSmsClient(const Codes::ptr& codes_client) : _codes_client(codes_client) {}
    
    // 與真實(shí)DMSClient接口完全一致,方便替換
    bool send(const std::string& phone, const std::string& code) {
        // 不調(diào)用外部平臺(tái),只打印日志(測(cè)試時(shí)能直接看到驗(yàn)證碼)
        LOG_INFO("【模擬短信】向{}發(fā)送驗(yàn)證碼:{}", phone, code);
        return true;
    }
private:
    Codes::ptr _codes_client;  // 用于后續(xù)擴(kuò)展,比如存驗(yàn)證碼
};

第二步:集成到 GetPhoneVerifyCode

void GetPhoneVerifyCode(...) {
    // 1. 校驗(yàn)手機(jī)號(hào)格式(11位,以1開(kāi)頭,第二位3-9)
    std::string phone = request->phone_number();
    if (!phone_check(phone)) {
        return err_response("手機(jī)號(hào)碼格式錯(cuò)誤!");
    }
    
    // 2. 生成4位驗(yàn)證碼(自定義工具函數(shù)vcode())
    std::string code_id = uuid();
    std::string code = vcode();  // 返回如"1234"
    
    // 3. 調(diào)用模擬短信服務(wù)(實(shí)際只打日志)
    if (!_dms_client->send(phone, code)) {
        return err_response("短信驗(yàn)證碼發(fā)送失敗!");
    }
    
    // 4. 存Redis(5分鐘過(guò)期)
    _redis_codes->append(code_id, code, std::chrono::minutes(5));
    
    // 5. 返回驗(yàn)證碼ID
    response->set_verify_code_id(code_id);
    response->set_success(true);
}

關(guān)鍵優(yōu)勢(shì):后來(lái)要對(duì)接真實(shí)短信服務(wù)時(shí),只需要實(shí)現(xiàn)一個(gè)RealSmsClient,保持send接口一致,在UserServerBuilder里換個(gè)注入對(duì)象就行,業(yè)務(wù)代碼一行不用改。

3. 登錄會(huì)話管理:Redis 防多端登錄

登錄成功后,要生成會(huì)話 ID(ssid),存 Redis,還要標(biāo)記用戶登錄狀態(tài),防止同一賬號(hào)多端登錄:

void UserLogin(...) {
    // 1. 校驗(yàn)用戶名密碼
    auto user = _mysql_user->select_by_nickname(nickname);
    if (!user || password != user->password()) {
        return err_response("用戶名或密碼錯(cuò)誤!");
    }
    
    // 2. 查是否已登錄(Redis查登錄狀態(tài))
    if (_redis_status->exists(user->user_id())) {
        return err_response("用戶已在其他地方登錄!");
    }
    
    // 3. 生成ssid,存Redis(2小時(shí)過(guò)期)
    std::string ssid = uuid();
    _redis_session->append(ssid, user->user_id(), std::chrono::hours(2));
    
    // 4. 標(biāo)記登錄狀態(tài)(2小時(shí)過(guò)期,與會(huì)話同步)
    _redis_status->append(user->user_id(), std::chrono::hours(2));
    
    // 5. 返回ssid
    response->set_login_session_id(ssid);
    response->set_success(true);
}

細(xì)節(jié)_redis_session_redis_status是封裝的 Redis 操作類,內(nèi)部調(diào)用sw::redis::Redis::set并設(shè)置過(guò)期時(shí)間,避免手動(dòng)寫(xiě) Redis 命令,減少出錯(cuò)概率。

4. 用戶信息修改:聯(lián)動(dòng)多存儲(chǔ)(MySQL+ES + 文件服務(wù))

以 “設(shè)置頭像” 為例,需要上傳頭像到文件服務(wù)→更新 MySQL 的 avatar_id→同步 ES 信息:

void SetUserAvatar(...) {
    // 1. 取用戶ID和頭像數(shù)據(jù)
    std::string uid = request->user_id();
    std::string avatar_data = request->avatar();
    
    // 2. 查用戶是否存在
    auto user = _mysql_user->select_by_id(uid);
    if (!user) {
        return err_response("未找到用戶信息!");
    }
    
    // 3. 調(diào)用文件服務(wù)上傳頭像(通過(guò)ServiceManager找文件服務(wù)節(jié)點(diǎn))
    auto channel = _mm_channels->choose(_file_service_name);
    FileService_Stub stub(channel.get());
    PutSingleFileReq file_req;
    PutSingleFileRsp file_rsp;
    file_req.mutable_file_data()->set_file_content(avatar_data);
    stub.PutSingleFile(&cntl, &file_req, &file_rsp, nullptr);
    if (cntl.Failed() || !file_rsp.success()) {
        return err_response("文件子服務(wù)調(diào)用失敗!");
    }
    
    // 4. 更新MySQL的avatar_id
    std::string avatar_id = file_rsp.file_info().file_id();
    user->avatar_id(avatar_id);
    if (!_mysql_user->update(user)) {
        return err_response("更新數(shù)據(jù)庫(kù)用戶頭像ID失敗!");
    }
    
    // 5. 同步ES信息
    if (!_es_user->appendData(user->user_id(), user->phone(), 
        user->nickname(), user->description(), avatar_id)) {
        return err_response("更新搜索引擎用戶頭像ID失敗!");
    }
    
    response->set_success(true);
}

經(jīng)驗(yàn):文件服務(wù)調(diào)用可能失敗,后來(lái)加了重試機(jī)制(失敗后重試 2 次),還在日志里打印文件服務(wù)的地址和錯(cuò)誤信息,方便定位是網(wǎng)絡(luò)問(wèn)題還是服務(wù)本身的問(wèn)題。

四、實(shí)戰(zhàn)踩坑記錄:這些問(wèn)題比代碼更重要

1. ODB 代碼生成遺漏導(dǎo)致鏈接錯(cuò)誤

最開(kāi)始用 ODB 的 ORM,只寫(xiě)了user.hxx,沒(méi)生成 ORM 實(shí)現(xiàn)代碼,編譯時(shí)出現(xiàn)一堆undefined reference to odb::access::object_traits_impl<zrt::User>錯(cuò)誤。

解決方法

  1. 安裝 ODB 工具:sudo apt install odb;

  2. 生成 ORM 代碼:odb -d mysql --std c++11 user.hxx -o source/;

  3. CMake 里添加生成的user-odb.cxx到源文件列表:

add_executable(user_server
    source/user_server.cc
    source/user-odb.cxx  # 必須加,否則鏈接不到ORM實(shí)現(xiàn)
)

2. Redis 客戶端初始化順序錯(cuò)誤

最開(kāi)始在make_redis_object里創(chuàng)建了Codes實(shí)例,但沒(méi)賦值給_codes_client,導(dǎo)致make_mock_sms_object時(shí)_codes_client是空的,運(yùn)行崩潰。

解決方法:調(diào)整初始化順序,確保 Redis 客戶端先初始化,再創(chuàng)建CodesMockSmsClient

// UserServerBuilder
void make_redis_object(...) {
    _redis_client = RedisClientFactory::create(...);
    // 初始化Codes,賦值給成員變量
    _codes_client = std::make_shared<Codes>(_redis_client);
}

void make_mock_sms_object() {
    // 此時(shí)_codes_client已初始化,不會(huì)空指針
    _mock_sms_client = std::make_shared<MockSmsClient>(_codes_client);
}

3. 依賴庫(kù)鏈接不全導(dǎo)致未定義錯(cuò)誤

編譯時(shí)出現(xiàn)undefined reference to sw::redis::Redis::set,是因?yàn)?CMake 沒(méi)鏈接swredis++hiredis庫(kù)。

解決方法:在 CMake 里添加鏈接:

# 查找依賴庫(kù)
find_package(swredis++ REQUIRED)
find_package(hiredis REQUIRED)

# 鏈接到目標(biāo)
target_link_libraries(user_server
    PRIVATE
    sw::redis++::swredis++
    hiredis::hiredis
    odb::mysql  # ODB MySQL庫(kù)
    elasticlient::elasticlient  # ES客戶端庫(kù)
    brpc  # RPC庫(kù)
    pthread  # 線程庫(kù)
)

五、總結(jié):UserServer 設(shè)計(jì)的 3 個(gè)核心要點(diǎn)

  1. 無(wú)狀態(tài)設(shè)計(jì):用戶服務(wù)不存本地?cái)?shù)據(jù)(會(huì)話、狀態(tài)都放 Redis),方便橫向擴(kuò)展,加節(jié)點(diǎn)就能扛更高并發(fā);

  2. 依賴注入優(yōu)先:所有外部依賴(短信、數(shù)據(jù)庫(kù)、緩存)都通過(guò)構(gòu)造函數(shù)注入,方便替換和測(cè)試,比如用模擬短信突破平臺(tái)限制;

  3. 分層清晰:業(yè)務(wù)邏輯(注冊(cè)登錄)、數(shù)據(jù)訪問(wèn)(ODB/Redis/ES)、服務(wù)調(diào)用(ServiceManager)分層,修改某一層不影響其他層。

做用戶服務(wù)時(shí),最容易忽略的是 “可測(cè)試性” 和 “容錯(cuò)性”—— 比如一開(kāi)始沒(méi)做模擬短信,導(dǎo)致沒(méi)法本地測(cè)試手機(jī)號(hào)登錄;沒(méi)加重試機(jī)制,文件服務(wù)偶爾超時(shí)就失敗。這些問(wèn)題都是實(shí)戰(zhàn)中踩出來(lái)的,比單純的代碼實(shí)現(xiàn)更有價(jià)值。

到此這篇關(guān)于C++微服務(wù)UserServer設(shè)計(jì)與實(shí)現(xiàn)方法的文章就介紹到這了,更多相關(guān)C++微服務(wù)UserServer實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 手把手教你如何一眼分辨是C還是C++

    手把手教你如何一眼分辨是C還是C++

    在很大程度上,C++是C的超集,這意味著一個(gè)有效的C程序也是一個(gè)有效的C++程序,下面這篇文章主要給大家介紹了關(guān)于如何一眼分辨是C還是C++的相關(guān)資料,需要的朋友可以參考下
    2023-02-02
  • C++多態(tài)的實(shí)現(xiàn)及原理詳細(xì)解析

    C++多態(tài)的實(shí)現(xiàn)及原理詳細(xì)解析

    C++的多態(tài)性用一句話概括就是:在基類的函數(shù)前加上virtual關(guān)鍵字,在派生類中重寫(xiě)該函數(shù),運(yùn)行時(shí)將會(huì)根據(jù)對(duì)象的實(shí)際類型來(lái)調(diào)用相應(yīng)的函數(shù)。如果對(duì)象類型是派生類,就調(diào)用派生類的函數(shù);如果對(duì)象類型是基類,就調(diào)用基類的函數(shù)
    2013-09-09
  • VSCode如何使用最新的C++20(推薦)

    VSCode如何使用最新的C++20(推薦)

    這篇文章主要介紹了VSCode使用最新的C++20的相關(guān)知識(shí),本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-03-03
  • C語(yǔ)言版三子棋游戲

    C語(yǔ)言版三子棋游戲

    這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言版三子棋游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • C++11, 14, 17對(duì)tuple元素的訪問(wèn)詳情

    C++11, 14, 17對(duì)tuple元素的訪問(wèn)詳情

    這篇文章主要介紹了C++11, 14, 17對(duì)tuple元素的訪問(wèn)詳情,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • 數(shù)組名不等于指針?sizeof()函數(shù)求數(shù)組大小錯(cuò)誤問(wèn)題及解決

    數(shù)組名不等于指針?sizeof()函數(shù)求數(shù)組大小錯(cuò)誤問(wèn)題及解決

    這篇文章主要介紹了數(shù)組名不等于指針?sizeof()函數(shù)求數(shù)組大小錯(cuò)誤問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • C++?用紅黑樹(shù)模擬實(shí)現(xiàn)set、map的示例代碼

    C++?用紅黑樹(shù)模擬實(shí)現(xiàn)set、map的示例代碼

    set、map的底層結(jié)構(gòu)是紅黑樹(shù),它們的函數(shù)通過(guò)調(diào)用紅黑樹(shù)的接口來(lái)實(shí)現(xiàn),本文主要介紹了C++?用紅黑樹(shù)模擬實(shí)現(xiàn)set、map,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-03-03
  • C++設(shè)計(jì)模式之迭代器模式

    C++設(shè)計(jì)模式之迭代器模式

    這篇文章主要介紹了C++設(shè)計(jì)模式之迭代器模式,本文講解了什么是迭代器模式、迭代器模式的代碼實(shí)例等內(nèi)容,需要的朋友可以參考下
    2014-10-10
  • 利用Matlab實(shí)現(xiàn)迭代適應(yīng)點(diǎn)算法

    利用Matlab實(shí)現(xiàn)迭代適應(yīng)點(diǎn)算法

    道格拉斯-普克算法(Douglas–Peucker?algorithm,亦稱為拉默-道格拉斯-普克算法、迭代適應(yīng)點(diǎn)算法、分裂與合并算法)是將曲線近似表示為一系列點(diǎn),并減少點(diǎn)的數(shù)量的一種算法。本文將利用Matlab實(shí)現(xiàn)這一算法,需要的可以參考一下
    2022-04-04
  • 一篇文章徹底弄懂C++虛函數(shù)的實(shí)現(xiàn)機(jī)制

    一篇文章徹底弄懂C++虛函數(shù)的實(shí)現(xiàn)機(jī)制

    C++中的虛函數(shù)的作用主要是實(shí)現(xiàn)了多態(tài)的機(jī)制,基類定義虛函數(shù),子類可以重寫(xiě)該函數(shù),在派生類中對(duì)基類定義的虛函數(shù)進(jìn)行重寫(xiě)時(shí),需要在派生類中聲明該方法為虛方法,這篇文章主要給大家介紹了關(guān)于如何通過(guò)一篇文章徹底弄懂C++虛函數(shù)的實(shí)現(xiàn)機(jī)制,需要的朋友可以參考下
    2021-06-06

最新評(píng)論

靖宇县| 肇东市| 依安县| 丽水市| 怀仁县| 敖汉旗| 华安县| 于田县| 天门市| 远安县| 封开县| 来凤县| 图片| 云霄县| 女性| 新巴尔虎左旗| 长乐市| 福安市| 河曲县| 海南省| 黄山市| 临夏市| 漳平市| 锡林郭勒盟| 常州市| 布拖县| 师宗县| 高州市| 巴马| 黄梅县| 兴宁市| 白城市| 广安市| 开平市| 房产| 绥阳县| 仁布县| 昌邑市| 亳州市| 屯留县| 灌阳县|