零基礎開啟元宇宙|如何快速創建虛擬形象

来源:https://www.cnblogs.com/RTCWang/archive/2022/12/12/16975874.html
-Advertisement-
Play Games

原文:Jgit的使用筆記 - Stars-One的雜貨小窩 之前整的一個系統,涉及到git代碼的推送,是通過cmd命令去推送的,然後最近在產品驗收的時候,測試部門隨意填了個git倉庫,然後導致倉庫代碼被覆蓋了,還好本地留有備份,沒出現啥大問題 然後就計劃於是就改為使用Jgit庫來實現推送代碼的功能, ...


元宇宙(Metaverse),是人類運用數字技術構建的,由現實世界映射或超越現實世界,可與現實世界交互的虛擬世界,具備新型社會體系的數字生活空間。

可見元宇宙第一步是創建專屬虛擬形象,但創建3D虛擬形象需要3D基礎知識。對於大部分android開發者(包括我本人)來說沒有這方面的積累。難道因此我們就難以進入元宇宙的世界嗎?不,今天我們藉助即構平臺提供的Avatar SDK,只要有Android基礎即可進入最火的元宇宙世界!先看效果:

請添加圖片描述

上面gif被壓縮的比較狠,這裡放一張截圖:

請添加圖片描述

1 免費註冊即構開發者

前往即構控制台網站:https://console.zego.im/註冊開發者賬戶。註冊成功後,創建項目:

請添加圖片描述

控制臺中可以得到AppID和AppSign兩個數據,這兩個數據是重要憑證,後面會用到。

由於我們用到了即構的Avatar功能,但目前官方沒有提供線上自動開啟方式,需要主動找客服申請(當然,這是免費的),只需提供自己項目的包名,即可開通Avatar許可權。打開https://doc-zh.zego.im/article/15206右下角有“聯繫我們”,點擊即可跟客服申請免費開通許可權。

註意,如果不向客服申請Avatar許可權,調用AvatarSDK會失敗!

2 準備開發環境

前往即構官方元宇宙開發SDK網站https://doc-zh.zego.im/article/15302下載SDK,得到如下文件列表:

請添加圖片描述

接下來過程如下:

  1. 打開SDK目錄,將裡面的ZegoAvatar.aar拷貝至app/libs目錄下。
  2. 添加SDK引用。打開app/build.gradle文件,在dependencies節點引入 libs下所有的jaraar:

dependencies {
 
    implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"]) //通配引入
    
    //其他略
}
  1. 設置許可權。根據實際應用需要,設置應用所需許可權。進入app/src/main/AndroidManifest.xml 文件,添加許可權。
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

因為Android 6.0在一些比較重要的許可權上要求必須申請動態許可權,不能只通過 AndroidMainfest.xml文件申請靜態許可權。具體動態請求許可權代碼可看附件源碼。

3 導入資源

上一小節下載的zip文件中,有個assets目錄。裡面包含了Avatar形象相關資源,如:衣服、眉毛、鞋子等。這是即構官方免費提供的資源,可以滿足一般性需求了。當然了,如果想要自己定製資源也是可以的。assets文件內容如下:

請添加圖片描述

資源名稱 說明
AIModel.bundle Avatar 的 AI 模型資源。當使用表情隨動、聲音隨動、AI 捏臉等能力時,必須先將該資源的絕對路徑設置給 Avatar SDK。
base.bundle 美術資源,包含基礎 3D 人物模型資源、資源映射表、人物模型預設外形等。
Packages 美妝、掛件、裝飾等資源。 每個資源 200 KB ~ 1 MB 不等,跟資源複雜度相關。

註意:由於資源文件很大,上面下載的美術資源只包含少量必須資源。如果需要全部資源,可以去官網找客服索要:https://doc-zh.zego.im/article/14882

上面的文件需要存放到Android本地SDCard上,這裡有2個方案可供參考:

  • 方案一: 將資源先放入到app/src/assets目錄內,然後在app啟動時,自動將assets的內容拷貝到SDcard中。
  • 方案二: 將資源放入到伺服器端,運行時自動從伺服器端下載。

為了簡單起見,我們這裡採用方案一。參考代碼如下, 詳細代碼可以看附件:

AssetsFileTransfer.copyAssetsDir2Phone(this.getApplication(),
            "AIModel.bundle", "assets");
AssetsFileTransfer.copyAssetsDir2Phone(this.getApplication(),
            "base.bundle", "assets");
AssetsFileTransfer.copyAssetsDir2Phone(this.getApplication(),
            "Packages", "assets");

4 創建虛擬形象

創建虛擬形象本質上來說就是調用即構的Avatar SDK,其大致流程如下:

請添加圖片描述

接下來我們逐步實現上面流程。

需要註意的是,上面示意圖中採用的是AvatarView,可以非常方便的直接展示Avatar形象,但是不方便後期將畫面通過RTC實時傳遞, 因此,我們後面的具體實現視通過TextureView替代AvatarView。

4.1 申請權鑒

這裡再次強調一下,一定要打開https://doc-zh.zego.im/article/15206點擊右下角有“聯繫我們”,向客服申請免費開通Avatar許可權。否則無法使用Avatar SDK

申請權鑒代碼如下:


public class KeyCenter { 
    // 控制臺地址: https://console.zego.im/dashboard 
    public static long APP_ID = 這裡值可以在控制台查詢,參考第一節;  //這裡填寫APPID
    public static String APP_SIGN =  這裡值可以在控制台查詢,參考第一節; 
    // 鑒權伺服器的地址
    public final static String BACKEND_API_URL = "https://aieffects-api.zego.im?Action=DescribeAvatarLicense";

    public static String avatarLicense = null;
    public static String getURL(String authInfo) {
        Uri.Builder builder = Uri.parse(BACKEND_API_URL).buildUpon();
        builder.appendQueryParameter("AppId", String.valueOf(APP_ID));
        builder.appendQueryParameter("AuthInfo", authInfo);

        return builder.build().toString();
    }

    public interface IGetLicenseCallback {
        void onGetLicense(int code, String message, ZegoLicense license);
    }

    /**
     * 線上拉取 license
     * @param context
     * @param callback
     */
    public static void getLicense(Context context, final IGetLicenseCallback callback) {
        requestLicense(ZegoAvatarService.getAuthInfo(APP_SIGN, context), callback);
    }

    /**
     * 獲取license
     * */
    public static void requestLicense(String authInfo, final IGetLicenseCallback callback) {

        String url = getURL(authInfo);

        HttpRequest.asyncGet(url, ZegoLicense.class, (code, message, responseJsonBean) -> {
            if (callback != null) {
                callback.onGetLicense(code, message, responseJsonBean);
            }
        });
    }


    public class ZegoLicense {
        @SerializedName("License")
        private String license;
        public String getLicense() {
            return license;
        }
        public void setLicense(String license) {
            this.license = license;
        }
    }

}

在獲取Lincense時,只需調用getLicense函數,例如在Activity類中只需如下調用:

KeyCenter.getLicense(this, (code, message, response) -> {
        if (code == 0) {
            KeyCenter.avatarLicense = response.getLicense();
            showLoading("正在初始化...");
            avatarMngr = AvatarMngr.getInstance(getApplication());
            avatarMngr.setLicense(KeyCenter.avatarLicense, this);
        } else {
            toast("License 獲取失敗, code: " + code);
        }
    });

4.2 初始化AvatarService

初始化AvatarService過程比較漫長(可能要幾秒),通過開啟worker線程後臺載入以避免主線程阻塞。因此我們定義一個回調函數,待完成初始化後回調通知:


public interface OnAvatarServiceInitSucced {
    void onInitSucced();
}

public void setLicense(String license, OnAvatarServiceInitSucced listener) {
    this.listener = listener;
    ZegoAvatarService.addServiceObserver(this);
    String aiPath = FileUtils.getPhonePath(mApp, "AIModel.bundle", "assets"); //   AI 模型的絕對路徑
    ZegoServiceConfig config = new ZegoServiceConfig(license, aiPath);
    ZegoAvatarService.init(mApp, config);
}

@Override
public void onStateChange(ZegoAvatarServiceState state) {
    if (state == ZegoAvatarServiceState.InitSucceed) {
        Log.i("ZegoAvatar", "Init success");
        // 要記得及時移除通知
        ZegoAvatarService.removeServiceObserver(this);
        if (listener != null) listener.onInitSucced();
    }
}

這裡setLicense函數內完成初始化AvatarService,初始化完成後會回調onStateChange函數。但是要註意,在初始化之前必須把資源文件拷貝到本地SDCard,即完成資源導入:

private void initRes(Application app) {
    // 先把資源拷貝到SD卡,註意:線上使用時,需要做一下判斷,避免多次拷貝。資源也可以做成從網路下載。
    if (!FileUtils.checkFile(app, "AIModel.bundle", "assets"))
        FileUtils.copyAssetsDir2Phone(app, "AIModel.bundle", "assets");
    if (!FileUtils.checkFile(app, "base.bundle", "assets"))
        FileUtils.copyAssetsDir2Phone(app, "base.bundle", "assets");
    if (!FileUtils.checkFile(app, "human.bundle", "assets"))
        FileUtils.copyAssetsDir2Phone(app, "human.bundle", "assets");
    if (!FileUtils.checkFile(app, "Packages", "assets"))
        FileUtils.copyAssetsDir2Phone(app, "Packages", "assets");
}

4.3 創建虛擬形象

前面在https://doc-zh.zego.im/article/15302下載SDK包含了helper目錄,這個目錄裡面有非常重要的兩個文件:

在這裡插入圖片描述

其中ZegoCharacterHelper文件是個介面定義類,即雖然是個類,但具體的實現全部在ZegoCharacterHelperImpl中。我們先一睹為快,看看ZegoCharacterHelper包含了哪些可處理的屬性:

public class ZegoCharacterHelper {
    public static final String MODEL_ID_MALE = "male";
    public static final String MODEL_ID_FEMALE = "female";
    //****************************** 捏臉維度的 key 值 ******************************/
    public static final String FACESHAPE_BROW_SIZE_Y = "faceshape_brow_size_y";// 眉毛厚度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_BROW_SIZE_X = "faceshape_brow_size_x";// 眉毛長度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_BROW_ALL_Y = "faceshape_brow_all_y";// 眉毛高度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_BROW_ALL_ROLL_Z = "faceshape_brow_all_roll_z";// 眉毛旋轉, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_EYE_SIZE = "faceshape_eye_size"; // 眼睛大小, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_EYE_SIZE_Y = "faceshape_eye_size_y";
    public static final String FACESHAPE_EYE_ROLL_Y = "faceshape_eye_roll_y";// 眼睛高度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_EYE_ROLL_Z = "faceshape_eye_roll_z";// 眼睛旋轉, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_EYE_X = "faceshape_eye_x";// 雙眼眼距, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_NOSE_ALL_X = "faceshape_nose_all_x";// 鼻子寬度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_NOSE_ALL_Y = "faceshape_nose_all_y";// 鼻子高度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_NOSE_SIZE_Z = "faceshape_nose_size_z";
    public static final String FACESHAPE_NOSE_ALL_ROLL_Y = "faceshape_nose_all_roll_y";// 鼻頭旋轉, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_NOSTRIL_ROLL_Y = "faceshape_nostril_roll_y";// 鼻翼旋轉, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_NOSTRIL_X = "faceshape_nostril_x";
    public static final String FACESHAPE_MOUTH_ALL_Y = "faceshape_mouth_all_y";// 嘴巴上下, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_LIP_ALL_SIZE_Y = "faceshape_lip_all_size_y";// 嘴唇厚度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_LIPCORNER_Y = "faceshape_lipcorner_y";// 嘴角旋轉, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_LIP_UPPER_SIZE_X = "faceshape_lip_upper_size_x"; // 上唇寬度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_LIP_LOWER_SIZE_X = "faceshape_lip_lower_size_x"; // 下唇寬度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_JAW_ALL_SIZE_X = "faceshape_jaw_all_size_x";// 下巴寬度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_JAW_Y = "faceshape_jaw_y";// 下巴高度, 取值範圍0.0-1.0,預設值0.5
    public static final String FACESHAPE_CHEEK_ALL_SIZE_X = "faceshape_cheek_all_size_x";// 臉頰寬度, 取值範圍0.0-1.0,預設值0.5

    //其他函數略
}

可以看到,上面數據基本包含所有人臉屬性了,基本具備了捏臉能力,篇幅原因,我們這裡不具體去實現。有這方面需求的讀者,可以通過在界面上調整上面相關屬性來實現。

接下來我們開始創建虛擬形象,首先創建一個User實體類:


public class User {
    public String userName; //用戶名
    public String userId; //用戶ID
    public boolean isMan; //性別
    public int width; //預覽寬度
    public int height; //預覽高度
    public int bgColor; //背景顏色
    public int shirtIdx = 0; // T-shirt資源id
    public int browIdx = 0; //眉毛資源id

    public User(String userName, String userId, int width, int height) {
        this.userName = userName;
        this.userId = userId;
        this.width = width;
        this.height = height;
        this.isMan = true;
        bgColor = Color.argb(255, 33, 66, 99);
    }  
}

示例作用,為了簡單起見,我們這裡只針對眉毛和衣服資源做選取。接下來創建一個Activity:

public class AvatarActivity extends BaseActivity  {
    private int vWidth = 720;
    private int vHeight = 1080; 
    private User user = new User("C_0001", "C_0001", vWidth, vHeight);
    private TextureView mTextureView;  //用於顯示Avatar形象
    private AvatarMngr mAvatarMngr; // 用於維護管理Avatar
    private ColorPickerDialog colorPickerDialog; //用於背景色選取

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_avatar);
        
        // ....
        // 其他初始化界面相關代碼略...
        // ....

        user.isMan = true;
        mTextureView = findViewById(R.id.avatar_view);  
        mZegoMngr = ZegoMngr.getInstance(getApplication()); 
        // 開啟虛擬形象預覽
        mAvatarMngr.start(mTextureView, user);
    }

最後一行代碼中開啟了虛擬形象預覽,那麼這裡開啟虛擬形象預覽具體做了哪些工作呢?show me the code:

public class AvatarMngr implements ZegoAvatarServiceDelegate, RTCMngr.CaptureListener {
    private static final String TAG = "AvatarMngr";
    private static AvatarMngr mInstance;
    private boolean mIsStop = false;

    private User mUser = null;
    private TextureBgRender mBgRender = null;
    private OnAvatarServiceInitSucced listener;
    private ZegoCharacterHelper mCharacterHelper;
    private Application mApp;

    public interface OnAvatarServiceInitSucced {
        void onInitSucced();
    }

    public void setLicense(String license, OnAvatarServiceInitSucced listener) {
        this.listener = listener;
        ZegoAvatarService.addServiceObserver(this);
        String aiPath = FileUtils.getPhonePath(mApp, "AIModel.bundle", "assets"); //   AI 模型的絕對路徑
        ZegoServiceConfig config = new ZegoServiceConfig(license, aiPath);
        ZegoAvatarService.init(mApp, config);
    }

    public void stop() {
        mIsStop = true;
        mUser = null;
        stopExpression();
    }

    public void updateUser(User user) {
        mUser = user;
        if (user.shirtIdx == 0) {
            mCharacterHelper.setPackage("m-shirt01");
        } else {
            mCharacterHelper.setPackage("m-shirt02");
        }
        if (user.browIdx == 0) {
            mCharacterHelper.setPackage("brows_1");
        } else {
            mCharacterHelper.setPackage("brows_2");
        }
    }

    /**
     * 啟動Avatar,調用此函數之前,請確保已經調用過setLicense
     *
     * @param avatarView
     */
    public void start(TextureView avatarView, User user) {
        mUser = user;
        mIsStop = false;
        initAvatar(avatarView, user);
        startExpression();
    }

    private void initAvatar(TextureView avatarView, User user) {
        String sex = ZegoCharacterHelper.MODEL_ID_MALE;
        if (!user.isMan) sex = ZegoCharacterHelper.MODEL_ID_FEMALE;

        // 創建 helper 簡化調用
        // base.bundle 是頭模, human.bundle 是全身人模
        mCharacterHelper = new ZegoCharacterHelper(FileUtils.getPhonePath(mApp, "human.bundle", "assets"));
        mCharacterHelper.setExtendPackagePath(FileUtils.getPhonePath(mApp, "Packages", "assets"));
        // 設置形象配置
        mCharacterHelper.setDefaultAvatar(sex);
        updateUser(user);
        // 獲取當前妝容數據, 可以保存到用戶資料中
        String json = mCharacterHelper.getAvatarJson();

    }

    // 啟動表情檢測
    private void startExpression() {
        // 啟動表情檢測前要申請攝像頭許可權, 這裡是在 MainActivity 已經申請過了
        ZegoAvatarService.getInteractEngine().startDetectExpression(ZegoExpressionDetectMode.Camera, expression -> {
            // 表情直接塞給 avatar 驅動
            mCharacterHelper.setExpression(expression);
        });
    }

    // 停止表情檢測
    private void stopExpression() {
        // 不用的時候記得停止
        ZegoAvatarService.getInteractEngine().stopDetectExpression();
    }

    // 獲取到 avatar 紋理後的處理
    public void onCaptureAvatar(int textureId, int width, int height) {
        if (mIsStop || mUser == null) { // rtc 的 onStop 是非同步的, 可能activity已經運行到onStop了, rtc還沒
            return;
        }
        boolean useFBO = true;
        if (mBgRender == null) {
            mBgRender = new TextureBgRender(textureId, useFBO, width, height, Texture2dProgram.ProgramType.TEXTURE_2D_BG);
        }
        mBgRender.setInputTexture(textureId);
        float r = Color.red(mUser.bgColor) / 255f;
        float g = Color.green(mUser.bgColor) / 255f;
        float b = Color.blue(mUser.bgColor) / 255f;
        float a = Color.alpha(mUser.bgColor) / 255f;
        mBgRender.setBgColor(r, g, b, a);
        mBgRender.draw(useFBO); // 畫到 fbo 上需要反向的
        ZegoExpressEngine.getEngine().sendCustomVideoCaptureTextureData(mBgRender.getOutputTextureID(), width, height, System.currentTimeMillis());


    }

    @Override
    public void onStartCapture() {
        if (mUser == null) return;
//        // 收到回調後,開發者需要執行啟動視頻採集相關的業務邏輯,例如開啟攝像頭等
        AvatarCaptureConfig config = new AvatarCaptureConfig(mUser.width, mUser.height);
//        // 開始捕獲紋理
        mCharacterHelper.startCaptureAvatar(config, this::onCaptureAvatar);
    }

    @Override
    public void onStopCapture() {
        mCharacterHelper.stopCaptureAvatar();
        stopExpression();
    }


    private void initRes(Application app) {
        // 先把資源拷貝到SD卡,註意:線上使用時,需要做一下判斷,避免多次拷貝。資源也可以做成從網路下載。
        if (!FileUtils.checkFile(app, "AIModel.bundle", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "AIModel.bundle", "assets");
        if (!FileUtils.checkFile(app, "base.bundle", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "base.bundle", "assets");
        if (!FileUtils.checkFile(app, "human.bundle", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "human.bundle", "assets");
        if (!FileUtils.checkFile(app, "Packages", "assets"))
            FileUtils.copyAssetsDir2Phone(app, "Packages", "assets");

    }

    @Override
    public void onError(ZegoAvatarErrorCode code, String desc) {
        Log.e(TAG, "errorcode : " + code.getErrorCode() + ",desc : " + desc);
    }

    @Override
    public void onStateChange(ZegoAvatarServiceState state) {
        if (state == ZegoAvatarServiceState.InitSucceed) {
            Log.i("ZegoAvatar", "Init success");
            // 要記得及時移除通知
            ZegoAvatarService.removeServiceObserver(this);
            if (listener != null) listener.onInitSucced();
        }
    }

    private AvatarMngr(Application app) {
        mApp = app;
        initRes(app);
    }

    public static AvatarMngr getInstance(Application app) {
        if (null == mInstance) {
            synchronized (AvatarMngr.class) {
                if (null == mInstance) {
                    mInstance = new AvatarMngr(app);
                }
            }
        }
        return mInstance;
    }
}

以上代碼完成了整個虛擬形象的創建,關鍵代碼全部展示,如果還需要具體全部代碼,直接從附件中下載即可。

5 附件


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 官方資料 官方解釋: https://pkg.go.dev/cmd/go#hdr-Build_constraints ,go help buildconstraint 也能看到描述 根據官方描述,go1.16開始建議使用go:build方式,與+build相比更容易被人閱讀。 有關go:build註 ...
  • keepalived 主備使用 本篇主要介紹一下 keepalived 的基本的 主備使用 1.概述 什麼是 keepalived呢,它是一個集群管理中 保證集群高可用的軟體,防止單點故障,keepalived是以VRRP協議為實現基礎的,VRRP全稱Virtual Router Redundanc ...
  • 有了非對稱密鑰、摘要、對稱密鑰等現代密碼學演算法與技術,是不是就能夠保證通信的安全無虞呢,並不是。 密碼學在互聯網應用的四個目標:機密性、完整性、身份驗證、防抵賴。到目前為止,我們討論的技術中,其中防抵賴的目標並沒有達到。 假設A、B、C三個人共用一個對稱加密演算法密鑰,現在A和B互相通信,A和B一直認 ...
  • 多線程程式 競態條件:多線程程式執行的結果是一致的,不會隨著CPU對線程不同的調用順序而產生不同的運行結果. 解決?:互斥鎖 mutex 經典的賣票問題,三個線程賣100張票 代碼1 #include <iostream> #include <thread> #include <list> #inc ...
  • C++語言層面多線程=>好處:跨平臺 windows/linux thread/mutex/condition_variable lock_gurad/unique_lock atomic/原子類型,基於CAS操作的原子類型 線程安全的 睡眠sleep_for C++ thread => windo ...
  • JZ45 把數組排成最小的數 描述 輸入一個非負整數數組numbers,把數組裡所有數字拼接起來排成一個數,列印能拼接出的所有數字中最小的一個。 例如輸入數組[3,32,321],則列印出這三個數字能排成的最小數字為321323。 1.輸出結果可能非常大,所以你需要返回一個字元串而不是整數 2.拼接 ...
  • 1. String 字元串是 Redis 最基本的數據類型,不僅所有 key 都是字元串類型,其它幾種數據類型構成的元素也是字元串。註意字元串的長度不能超過 512M。 1.1 編碼方式(encoding) 字元串對象的編碼可以是 int ,raw 或者 embstr 。 int 編碼:保存的是可以 ...
  • 應用背景: 隨著科學技術的發展,崗位數量越來越多,特別是每逢畢業季找工作的人數也很多,如果人們找工作或者企業招人靠純手工的話,費時費力,僅僅是篩選簡歷和費勁,並且員工找工作投簡歷可能得需要剋服時間和空間上的困難。所以為了方便員工找工作和企業招人,節約時間,特此開發員工招聘系統。(個人課設) 用例圖( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...