springboot3 集成mybatis 和通用mapper

来源:https://www.cnblogs.com/binz/archive/2023/08/28/17654403.html
-Advertisement-
Play Games

你的Java服務是如何監控的呢? 1.Null:監控?什麼監控?我一個寫代碼的服務掛了跟我有什麼關係? 2.命令行:服務掛了?記憶體泄漏?jstat jmap jcmd,還好不是我寫的 3.擼代碼:Java採集JVM/伺服器資源信息 -> Prometheus -> Grafana,請允許我對業務代碼 ...


xml版本查看:https://www.cnblogs.com/binz/p/6564490.html

springboot3.x以前的版本查看 https://www.cnblogs.com/binz/p/17421063.html

springboot3.x查看  https://www.cnblogs.com/binz/p/17654403.html

1、pom引用

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.2</version>
 </parent>

<dependencies>
     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <!-- web 容器使用 undertow 性能更強 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.7</version>
        </dependency>
<!-- 以前的tkmapper 不支持springboot3.x -->
<!-- 具體查看https://mapper.mybatis.io/--> <dependency> <groupId>io.mybatis</groupId> <artifactId>mybatis-mapper</artifactId> <version>2.1.1</version> </dependency>
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency>


    <!-- openapi、 swagger3、knife4j配置,適用boot3 -->
    <!-- https://doc.xiaominfo.com -->
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
      <version>4.3.0</version>
    </dependency>
    <!-- openapi配置 -->

    
</dependencies>    

2、新建自己的BaseMapper

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.apache.ibatis.session.RowBounds;

import com.github.pagehelper.Page;
import cn.hutool.core.bean.BeanUtil;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import io.mybatis.mapper.Mapper;
import io.mybatis.mapper.example.Example;
import io.mybatis.mapper.example.LambdaExampleWrapper;

public interface BaseMapper<T> extends Mapper<T,Long>{
    
    List<T> selectList(T entity, RowBounds rowBounds);

    default T getById(Long id) {
        return selectByPrimaryKey(id).orElse(null);
    }

    default <RE extends Serializable> RE getById(Long id,Class<RE> returnClass) {
        T t = getById(id);
        if(t != null) {
            return covert(t,returnClass);
        }
        return null;
    }

    default T one(T query) {
        return selectOne(query).orElse(null);
    }


    default <RE extends Serializable> RE one(T query,Class<RE> returnClass) {
        Optional<T> optional = selectOne(query);
        if(optional.isPresent()) {
            T t = optional.get();
            return covert(t,returnClass);
        }
        return null;
    }


    default <RE extends Serializable> List<RE> select(T t,Class<RE> returnClass) {
        List<T> ts = selectList(t);
        return covertList(ts,returnClass);
    }

    default <RE extends Serializable> Page<RE> selectPage(T t,Class<RE> returnClass) {
        Page<T> ts = (Page<T>) selectList(t);
        return (Page<RE>) covertList(ts,returnClass);
    }

    default <RE extends Serializable> Page<RE> selectPageByExample(Example<T> example,Class<RE> returnClass) {
        Page<T> ts = (Page<T>) selectByExample(example);
        return (Page<RE>) covertList(ts,returnClass);
    }



    default <RE extends Serializable> List<RE> selectByExample(Example<T> example,Class<RE> returnClass) {
        List<T> ts = selectByExample(example);
        return covertList(ts,returnClass);
    }

    default <RE extends Serializable> RE selectOneByExample(Example<T> example,Class<RE> returnClass) {
        Optional<T> optional = selectOneByExample(example);
        if(optional.isPresent()) {
            T t = optional.get();
            return covert(t,returnClass);
        }
        return null;
    }

    default <RE extends Serializable> RE selectOneByExampleLimitOne(Example<T> example,Class<RE> returnClass) {
        T t = selectOneByExampleLimitOne(example);
        if(t != null) {
            return covert(t, returnClass);
        }
        return null;
    }

    default T selectOneByExampleLimitOne(Example<T> example) {
        RowBounds rowBounds = new RowBounds(0, 1);
        List<T> ts = selectByExample(example, rowBounds);
        if(ObjectUtil.isNotEmpty(ts)) {
            return ts.get(0);
        }
        return null;
    }



    default T selectOneByLimitOne(T t) {
        RowBounds rowBounds = new RowBounds(0, 1);
        List<T> ts = selectList(t,rowBounds);
        if(ObjectUtil.isNotEmpty(ts)) {
            return ts.get(0);
        }
        return null;
    }


    @SuppressWarnings("unchecked")
    default Class<T> thisTClass() {
        Class<?> class1 = getClass();
        Class<?> interfaces = class1.getInterfaces()[0];
        Type[] genericInterfaces = interfaces.getGenericInterfaces();
        Type type = genericInterfaces[0];
        if( type instanceof ParameterizedType){
            ParameterizedType pType = (ParameterizedType) type;
            Type clazz = pType.getActualTypeArguments()[0];
            if( clazz instanceof Class<?> ){
                return (Class<T>) clazz;
            }
        }
        return null;
    }

    default <RE extends Serializable> List<RE> covertList(List<T> ts,Class<RE> returnClass){
        List<RE> responses;
        if(ts instanceof Page) {
            responses = new Page<>();
        }else {
            responses = new ArrayList<>();
        }
        for (T t : ts) {
            responses.add(covert(t,returnClass));
        }
        return responses;
    }

    default <RE extends Serializable> RE covert(T t , Class<RE> returnClass) {
        if(t != null) {
            RE response = null;
            try {
                response = ReflectUtil.newInstanceIfPossible(returnClass);
                BeanUtil.copy(t, response);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return response;
        }
        return null;
    }

    //自帶的wrapper()個人覺得還缺點東西,就自己複製了一份出來微調了,根據情況使用,不需要就刪除,需要對應的代碼在下方
    default LambdaExampleWrapper<T> lambdaWrapper() {
        return new LambdaExampleWrapper<>(BaseMapper.this, example());
    }

}
View Code

3、新建表模型,註意註解使用和以前版本有區別,但是相容一些之前javax.persistence的一些基本註解

import java.util.Date;

import io.mybatis.provider.Entity;
import lombok.Data;

@Data
@Entity.Table("system_user")
public class User {

    @Entity.Column(id = true,insertable = false,updatable = false)
    private Long id;

    /**
    * 姓名
    */
    private String realname;

    /**
    * 手機號
    */
    private String mobile;

    /**
    * 密碼
    */
    private String password;

    /**
    * 身份證號
    */
    private String idcard;
    

    /**
    * 頭像
    */
    private String avatar;

    /**
    * 最後登錄時間
    */
    private String lastLoginIp;

    /**
    * 最後登錄時間
    */
    private Date lastLoginTime;

    /**
    * 創建人
    */
    private Long createBy;

    /**
    * 創建時間
    */
    private Date createTime;

    /**
    * 修改人
    */
    private Long updateBy;

    /**
    * update_time
    */
    private Date updateTime;


}
View Code

4、創建對應業務的mapper繼承BaseMapper

import com.xxx.core.base.BaseMapper;
import com.xxx.system.model.User;

public interface UserMapper extends BaseMapper<User>{


}

5、啟動類掃描自己的mapper目錄 @MapperScan

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableCaching
@EnableScheduling
@SpringBootApplication
@MapperScan(basePackages="com.xxx.*.mapper")
@ComponentScan(basePackages = { "com.xxx.*.config", "com.xxx.*.controller", "com.xxx.*.job" })
public class OperateApiStarted {
    public static void main(String[] args) {
        SpringApplication.run(OperateApiStarted.class, args);
    }
}

 6、創建io.mybatis.mapper.example.LambdaExampleWrapper

package io.mybatis.mapper.example;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.ibatis.exceptions.TooManyResultsException;
import org.apache.ibatis.session.RowBounds;

import com.github.pagehelper.Page;
import com.xxx.BaseMapper;
import com.xxx.BeanUtil;

import io.mybatis.common.util.Assert;
import io.mybatis.mapper.fn.Fn;

public class LambdaExampleWrapper<T> {


    private final BaseMapper<T>    baseMapper;
    private final Example<T>          example;
    private       Example.Criteria<T> current;

    public LambdaExampleWrapper(BaseMapper<T> baseMapper, Example<T> example) {
        this.baseMapper = baseMapper;
        this.example = example;
        this.current = example.createCriteria();
    }

    /**
     * or 一組條件
     *
     * @return 條件
     */
    public LambdaExampleWrapper<T> or() {
        this.current = this.example.or();
        return this;
    }

    /**
     * 獲取查詢條件
     */
    public Example<T> example() {
        return example;
    }

    /**
     * 清除條件,可重用
     */
    public LambdaExampleWrapper<T> clear() {
        this.example.clear();
        this.current = example.createCriteria();
        return this;
    }

    /**
     * 指定查詢列
     *
     * @param fns 方法引用
     */
    @SafeVarargs
    public final LambdaExampleWrapper<T> select(Fn<T, Object>... fns) {
        this.example.selectColumns(fns);
        return this;
    }

    /**
     * 排除指定查詢列
     *
     * @param fns 方法引用
     */
    @SafeVarargs
    public final LambdaExampleWrapper<T> exclude(Fn<T, Object>... fns) {
        this.example.excludeColumns(fns);
        return this;
    }

    /**
     * 設置起始 SQL
     *
     * @param startSql 起始 SQL,添加到 SQL 前,註意防止 SQL 註入
     */
    public LambdaExampleWrapper<T> startSql(String startSql) {
        this.example.setStartSql(startSql);
        return this;
    }

    /**
     * 設置結尾 SQL
     *
     * @param endSql 結尾 SQL,添加到 SQL 最後,註意防止 SQL 註入
     */
    public LambdaExampleWrapper<T> endSql(String endSql) {
        this.example.setEndSql(endSql);
        return this;
    }

    /**
     * 通過方法引用方式設置排序欄位
     *
     * @param fn    排序列的方法引用
     * @param order 排序方式
     * @return Example
     */
    public LambdaExampleWrapper<T> orderBy(Fn<T, Object> fn, Example.Order order) {
        this.example.orderBy(fn, order);
        return this;
    }

    /**
     * 支持使用字元串形式來設置 order by,用以支持一些特殊的排序方案
     *
     * @param orderByCondition 排序字元串(不會覆蓋已有的排序內容)
     * @return Example
     */
    public LambdaExampleWrapper<T> orderBy(String orderByCondition) {
        this.example.orderBy(orderByCondition);
        return this;
    }

    /**
     * 支持使用字元串形式來設置 order by,用以支持一些特殊的排序方案
     *
     * @param orderByCondition 排序字元串構造方法,比如通過數組集合迴圈拼接等
     * @return Example
     */
    public LambdaExampleWrapper<T> orderBy(Supplier<String> orderByCondition) {
        this.example.orderBy(orderByCondition);
        return this;
    }

    /**
     * 支持使用字元串形式來設置 order by,用以支持一些特殊的排序方案
     *
     * @param useOrderBy       條件表達式,true使用,false不使用 字元串排序
     * @param orderByCondition 排序字元串構造方法,比如通過數組集合迴圈拼接等
     * @return Example
     */
    public LambdaExampleWrapper<T> orderBy(boolean useOrderBy, Supplier<String> orderByCondition) {
        return useOrderBy ? this.orderBy(orderByCondition) : this;
    }

    /**
     * 通過方法引用方式設置排序欄位,升序排序
     *
     * @param fns 排序列的方法引用
     * @return Example
     */
    @SafeVarargs
    public final LambdaExampleWrapper<T> orderByAsc(Fn<T, Object>... fns) {
        this.example.orderByAsc(fns);
        return this;
    }

    /**
     * 通過方法引用方式設置排序欄位,降序排序
     *
     * @param fns 排序列的方法引用
     * @return Example
     */
    @SafeVarargs
    public final LambdaExampleWrapper<T> orderByDesc(Fn<T, Object>... fns) {
        this.example.orderByDesc(fns);
        return this;
    }

    /**
     * 設置 distince
     */
    public LambdaExampleWrapper<T> distinct() {
        this.example.setDistinct(true);
        return this;
    }

    /**
     * 設置更新欄位和值
     *
     * @param useSet 表達式條件, true 使用,false 不使用
     * @param setSql "column = value"
     */
    public LambdaExampleWrapper<T> set(boolean useSet, String setSql) {
        return useSet ? set(setSql) : this;
    }

    /**
     * 設置更新欄位和值
     *
     * @param setSql "column = value"
     */
    public LambdaExampleWrapper<T> set(String setSql) {
        this.example.set(setSql);
        return this;
    }

    /**
     * 設置更新欄位和值
     *
     * @param useSet 表達式條件, true 使用,false 不使用
     * @param fn     欄位
     * @param value  值
     */
    public LambdaExampleWrapper<T> set(boolean useSet, Fn<T, Object> fn, Object value) {
        return useSet ? set(fn, value) : this;
    }


    /**
     * 設置更新欄位和值
     *
     * @param useSet   表達式條件, true 使用,false 不使用
     * @param fn       欄位
     * @param supplier 值構造函數
     */
    public LambdaExampleWrapper<T> set(boolean useSet, Fn<T, Object> fn, Supplier<Object> supplier) {
        return useSet ? set(fn, supplier.get()) : this;
    }


    /**
     * 設置更新欄位和值
     *
     * @param fn    欄位
     * @param value 值
     */
    public LambdaExampleWrapper<T> set(Fn<T, Object> fn, Object value) {
        this.example.set(fn, value);
        return this;
    }

    /**
     * 指定欄位為 null
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     */
    public LambdaExampleWrapper<T> isNull(boolean useCondition, Fn<T, Object> fn) {
        return useCondition ? isNull(fn) : this;
    }

    /**
     * 指定欄位為 null
     *
     * @param fn 欄位對應的 get 方法引用
     */
    public LambdaExampleWrapper<T> isNull(Fn<T, Object> fn) {
        this.current.addCriterion(fn.toColumn() + " IS NULL");
        return this;
    }

    /**
     * 指定欄位不為 null
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     */
    public LambdaExampleWrapper<T> isNotNull(boolean useCondition, Fn<T, Object> fn) {
        return useCondition ? isNotNull(fn) : this;
    }

    /**
     * 指定欄位不為 null
     *
     * @param fn 欄位對應的 get 方法引用
     */
    public LambdaExampleWrapper<T> isNotNull(Fn<T, Object> fn) {
        this.current.addCriterion(fn.toColumn() + " IS NOT NULL");
        return this;
    }

    /**
     * 欄位 = 值
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     * @param value        值
     */
    public LambdaExampleWrapper<T> eq(boolean useCondition, Fn<T, Object> fn, Object value) {
        return useCondition ? eq(fn, value) : this;
    }

    /**
     * 欄位 = 值
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     * @param supplier     值構造函數
     */
    public LambdaExampleWrapper<T> eq(boolean useCondition, Fn<T, Object> fn, Supplier<Object> supplier) {
        return useCondition ? eq(fn, supplier.get()) : this;
    }

    /**
     * 欄位 = 值
     *
     * @param fn    欄位對應的 get 方法引用
     * @param value 值
     */
    public LambdaExampleWrapper<T> eq(Fn<T, Object> fn, Object value) {
        this.current.addCriterion(fn.toColumn() + " =", value);
        return this;
    }

    /**
     * 欄位 != 值
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     * @param supplier     值構造函數
     */
    public LambdaExampleWrapper<T> ne(boolean useCondition, Fn<T, Object> fn, Supplier<Object> supplier) {
        return useCondition ? ne(fn, supplier.get()) : this;
    }

    /**
     * 欄位 != 值
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     * @param value        值
     */
    public LambdaExampleWrapper<T> ne(boolean useCondition, Fn<T, Object> fn, Object value) {
        return useCondition ? ne(fn, value) : this;
    }

    /**
     * 欄位 != 值
     *
     * @param fn    欄位對應的 get 方法引用
     * @param value 值
     */
    public LambdaExampleWrapper<T> ne(Fn<T, Object> fn, Object value) {
        this.current.addCriterion(fn.toColumn() + " <>", value);
        return this;
    }

    /**
     * 欄位 > 值
     *
     * @param useCondition 表達式條件, true 使用,false 不使用
     * @param fn           欄位對應的 get 方法引用
     * @param supplier     值構造函數
     */
    public LambdaExampleWrapper<T> gt(boolean useCondition, Fn<T, Object> fn, Supplier<Object> supplier) {
        return useCondition ? gt(fn, supplier.get()) : this;
    }

    /**	   

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

-Advertisement-
Play Games
更多相關文章
  • ##### 17 JavaScript 中的call和apply 對於咱們逆向工程師而言. 並不需要深入的理解call和apply的本質作用. 只需要知道這玩意執行起來的邏輯順序是什麼即可 在運行時. 正常的js調用: ```js function People(name, age){ this.n ...
  • 地圖在 app 中使用還是很廣泛的,常見的應用常見有: 1、獲取自己的位置,規劃路線。 2、使用標記點進行標記多個位置。 3、繪製多邊形,使用圍牆標記位置等等。 此篇文章就以高德地圖為例,以上述三個常見需求為例,教大家如何在 uniapp 中添加地圖。 作為一個不管閑事的前端姑娘,我就忽略掉那些繁瑣 ...
  • # 項目主體搭建 - 前端:`vue3`、`element-plus`、`ts`、`axios`、`vue-router`、`pinia` - 後端:`nodejs`、`koa`、`koa-router`、`koa-body`、`jsonwebtoken` - 部署:`nginx`、`pm2`、`x ...
  • 前端實現文件下載功能有多種方法,這裡就不一一介紹,這裡只介紹使用文件流下載的實現方法。 既然是文件流那就肯定需要給前端返回一堆二進位編碼,作為後端就可以返回一個OutPutStream 後端可以使用Java中servlet提供的HttpServletResponse,核心步驟是要設置響應的數據類型, ...
  • Printf() 函數可以使用多種格式化動詞對輸出進行格式化。下麵是可以與所有數據類型一起使用的一些通用格式化動詞: **通用格式化動詞:** 以下動詞適用於所有數據類型: |動詞|描述| |-|-| |`%v`|以預設格式列印值| |`%#v`|以 Go 語法格式列印值| |`%T`|列印值的類型 ...
  • `pandas`小技巧系列是介紹的是使用`pandas`分析數據時,最常用的一些操作技巧。 具體包括: 1. [創建測試數據](https://www.cnblogs.com/wang_yb/p/17552748.html) 學習pandas的過程中,為了嘗試pandas提供的各類功能強大的函數,常 ...
  • ## 1. 什麼是WebSocket? WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協議,它允許在瀏覽器和伺服器之間進行實時的、雙向的通信。相對於傳統的基於請求和響應的 HTTP 協議,WebSocket 提供了一種更有效、更實時的通信方式,適用於需要實時更新、實時通知和實時交互 ...
  • 來源:toutiao.com/article/7234104886726705716 ## 1.前言 我們的生產環境基本上都部署在雲伺服器上,例如應用伺服器、MySQL伺服器等。如果MySQL伺服器直接暴露在公網,就會存在很大的風險,為了保證數據安全,MySQL伺服器的埠是不對外開放的。 好巧不巧 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...