APP添加人脸识别功能

心血来潮想要给最近开发的“到了没”加一个人脸签到的新功能,于是便开干了!
采用的是科大讯飞的人脸识别接口。
集成过程还是和以前一样:
1.下载jdk,并且添加到项目中
这里写图片描述
2.在云端创建应用,并生成相关的appid等值。
这里写图片描述
3.读相关的开发文档,进行相关操作即可

人脸的注册,以及用户分组等功能,我写到一个独立的demo中,就不多展示,只展示“到了没” 1:N人脸验证的相关代码。
代码如下:

FaceRollcall.class

package com.example.FaceIdentify;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.example.database.DBHelper;
import com.example.database.Student;
import com.example.database.StudentBase;
import com.example.rollcall.MainActivity;
import com.example.rollcall.R;
import com.example.rollcall.Topbar;
import com.iflytek.cloud.ErrorCode;
import com.iflytek.cloud.IdentityListener;
import com.iflytek.cloud.IdentityResult;
import com.iflytek.cloud.IdentityVerifier;
import com.iflytek.cloud.InitListener;
import com.iflytek.cloud.SpeechConstant;
import com.iflytek.cloud.SpeechError;
import com.iflytek.cloud.SpeechUtility;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import cn.bmob.v3.BmobQuery;
import cn.bmob.v3.listener.FindListener;

import static android.R.attr.id;

/**
 * Created by Cactus on 2018/2/10.
 */

public class FaceRollcall extends Activity implements View.OnClickListener {

    private final static String TAG = FaceRollcall.class.getSimpleName();

    // 用户输入的组ID
    private String mGroupId;
    // 身份鉴别对象
    private IdentityVerifier mIdVerifier;
    private ProgressDialog mProDialog;
    private Toast mToast;
    JSONArray candidates;
    private static DBHelper dbHelper;
    private static SQLiteDatabase database;

    // 选择图片后返回
    public static final int REQUEST_PICTURE_CHOOSE = 1;
    // 拍照后返回
    private final static int REQUEST_CAMERA_IMAGE = 2;
    // 裁剪图片成功后返回
    public static final int REQUEST_INTENT_CROP = 3;

    private Bitmap mImageBitmap = null;
    private byte[] mImageData = null;
    private File mPictureFile;
    private EditText et_meeting;
    List<StudentBase> studentList = new ArrayList<StudentBase>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_face_identify);
        Topbar topBar = (Topbar) findViewById(R.id.topbar_face);
        topBar.setOnLeftAndRightClickListener(new Topbar.OnLeftAndRightClickListener() {
            @Override
            public void OnLeftButtonClick() {
                Intent i = getIntent();
                i.setClass(getApplicationContext(),MainActivity.class);
                startActivity(i);
            }

            @Override
            public void OnRightButtonClick() {
            }
        });
        SpeechUtility.createUtility(this, SpeechConstant.APPID +"=输入自己的APPid");
        // 对象初始化监听器
        mIdVerifier = IdentityVerifier.createVerifier(FaceRollcall.this, new InitListener() {
            @Override
            public void onInit(int errorCode) {
                if (ErrorCode.SUCCESS == errorCode) {
                    showTip("引擎初始化成功");
                } else {
                    showTip("引擎初始化失败,错误码:" + errorCode);
                }
            }
        });

        // 初始化界面
        initUI();
        // 身份验证对象初始化
    }

    private void initUI() {
        findViewById(R.id.button_face).setOnClickListener(FaceRollcall.this);
        findViewById(R.id.button_face_rollcall).setOnClickListener(FaceRollcall.this);
        et_meeting = (EditText) findViewById(R.id.et_meeting);
        et_meeting.setOnClickListener(this);
        mProDialog = new ProgressDialog(FaceRollcall.this);
        mProDialog.setCancelable(true);
        mProDialog.setTitle("请稍候");
        // cancel进度框时,取消正在进行的操作
        mProDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {

            @Override
            public void onCancel(DialogInterface dialog) {
                if (null != mIdVerifier) {
                    mIdVerifier.cancel();
                }
            }
        });

        mToast = Toast.makeText(FaceRollcall.this, "", Toast.LENGTH_SHORT);
        mToast.setGravity(Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL, 0, 0);
    }

    /**
     * 人脸鉴别监听器
     */
    private IdentityListener mSearchListener = new IdentityListener() {

        @Override
        public void onResult(IdentityResult result, boolean islast) {
            Log.d(TAG, result.getResultString());

            dismissProDialog();

            handleResult(result);
        }

        @Override
        public void onEvent(int eventType, int arg1, int arg2, Bundle obj) {
        }

        @Override
        public void onError(SpeechError error) {
            dismissProDialog();

            showTip(error.getPlainDescription(true));
        }

    };

    @Override
    public void onClick(View view) {
        if( null == mIdVerifier ){
            // 创建单例失败,与 21001 错误为同样原因,参考 http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=9688
            showTip( "创建对象失败,请确认 libmsc.so 放置正确,且有调用 createUtility 进行初始化" );
            return;
        }

        switch (view.getId()) {
            case R.id.button_face:
                // 设置相机拍照后照片保存路径
                mPictureFile = new File(Environment.getExternalStorageDirectory(),
                        "picture" + System.currentTimeMillis()/1000 + ".jpg");
                // 启动拍照,并保存到临时文件
                Intent mIntent = new Intent();
                mIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
                mIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mPictureFile));
                mIntent.putExtra(MediaStore.Images.Media.ORIENTATION, 0);
                startActivityForResult(mIntent, REQUEST_CAMERA_IMAGE);
                break;

            case R.id.button_face_rollcall:
                    mGroupId = et_meeting.getText().toString();
                    if (null != mImageData) {
                        mProDialog.setMessage("鉴别中...");
                        mProDialog.show();
                        // 清空参数
                        mIdVerifier.setParameter(SpeechConstant.PARAMS, null);
                        // 设置业务场景
                        mIdVerifier.setParameter(SpeechConstant.MFV_SCENES, "ifr");
                        // 设置业务类型
                        mIdVerifier.setParameter(SpeechConstant.MFV_SST, "identify");
                        // 设置监听器,开始会话
                        mIdVerifier.startWorking(mSearchListener);
                        // 子业务执行参数,若无可以传空字符传
                        StringBuffer params = new StringBuffer();
                        params.append(",group_id=" + mGroupId +",topc=3");
                        // 向子业务写入数据,人脸数据可以一次写入
                        mIdVerifier.writeData("ifr", params.toString(), mImageData, 0, mImageData.length);
                        // 写入完毕
                        mIdVerifier.stopWrite("ifr");
                    } else {
                        showTip("请选择图片后再鉴别");
                    }
                break;
            default:
                break;
        }
    }

    protected void handleResult(IdentityResult result) {
        if (null == result) {
            return;
        }

        try {
            String resultStr = result.getResultString();
            JSONObject resultJson = new JSONObject(resultStr);
            if(ErrorCode.SUCCESS == resultJson.getInt("ret"))
            {
                JSONObject ifv_result = resultJson.getJSONObject("ifv_result");
                candidates = ifv_result.getJSONArray("candidates");
                JSONObject obj = candidates.getJSONObject(0);
                String userId = obj.optString("user");
                studentList = query_name(this,userId);
                String stu_name = studentList.get(0).getName();
                int signFlag_add = studentList.get(0).getSignFlag();
                update(this,userId,signFlag_add+1);
                showTip(stu_name+" 签到成功");
            }
            else {
                showTip("鉴别失败!");
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != RESULT_OK) {
            return;
        }

        String fileSrc = null;
        if (requestCode == REQUEST_PICTURE_CHOOSE ) {
            if ("file".equals(data.getData().getScheme())) {
                // 有些低版本机型返回的Uri模式为file
                fileSrc = data.getData().getPath();
            } else {
                // Uri模型为content
                String[] proj = {MediaStore.Images.Media.DATA};
                Cursor cursor = getContentResolver().query(data.getData(), proj,
                        null, null, null);
                cursor.moveToFirst();
                int idx = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                fileSrc = cursor.getString(idx);
                cursor.close();
            }
            // 跳转到图片裁剪页面
            cropPicture(this,Uri.fromFile(new File(fileSrc)));
        } else if (requestCode == REQUEST_CAMERA_IMAGE) {
            if (null == mPictureFile) {
                showTip("拍照失败,请重试");
                return;
            }

            fileSrc = mPictureFile.getAbsolutePath();
            updateGallery(fileSrc);
            // 跳转到图片裁剪页面
            cropPicture(this,Uri.fromFile(new File(fileSrc)));
        } else if (requestCode == REQUEST_INTENT_CROP) {

            // 获取返回数据
            Bitmap bmp = data.getParcelableExtra("data");

            // 获取裁剪后图片保存路径
            fileSrc = getImagePath();

            // 若返回数据不为null,保存至本地,防止裁剪时未能正常保存
            if(null != bmp){
                saveBitmapToFile(bmp);
            }

            // 获取图片的宽和高
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            mImageBitmap = BitmapFactory.decodeFile(fileSrc, options);

            // 压缩图片
            options.inSampleSize = Math.max(1, (int) Math.ceil(Math.max(
                    (double) options.outWidth / 1024f,
                    (double) options.outHeight / 1024f)));
            options.inJustDecodeBounds = false;
            mImageBitmap = BitmapFactory.decodeFile(fileSrc, options);

            // 若mImageBitmap为空则图片信息不能正常获取
            if(null == mImageBitmap) {
                showTip("图片信息无法正常获取!");
                return;
            }

            // 部分手机会对图片做旋转,这里检测旋转角度
            int degree = readPictureDegree(fileSrc);
            if (degree != 0) {
                // 把图片旋转为正的方向
                mImageBitmap = rotateImage(degree, mImageBitmap);
            }

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            //可根据流量及网络状况对图片进行压缩
            mImageBitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
            mImageData = baos.toByteArray();

            ((ImageView) findViewById(R.id.online_img)).setImageBitmap(mImageBitmap);
        }
    }

    @Override
    public void finish() {
        if (null != mProDialog) {
            mProDialog.dismiss();
        }
        setResult(RESULT_OK);
        super.finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (null != mIdVerifier) {
            mIdVerifier.destroy();
            mIdVerifier = null;
        }
    }

    /**
     * 读取图片属性:旋转的角度
     *
     * @param path 图片绝对路径
     * @return degree 旋转的角度
     */
    public static int readPictureDegree(String path) {
        int degree = 0;
        try {
            ExifInterface exifInterface = new ExifInterface(path);
            int orientation = exifInterface.getAttributeInt(
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return degree;
    }

    private void updateGallery(String filename) {
        MediaScannerConnection.scanFile(this, new String[] {filename}, null,
                new MediaScannerConnection.OnScanCompletedListener() {

                    @Override
                    public void onScanCompleted(String path, Uri uri) {

                    }
                });
    }

    /**
     * 旋转图片
     *
     * @param angle
     * @param bitmap
     * @return Bitmap
     */
    public static Bitmap rotateImage(int angle, Bitmap bitmap) {
        // 图片旋转矩阵
        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        // 得到旋转后的图片
        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
                bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        return resizedBitmap;
    }

    /**
     * Toast弹出提示
     * @param str
     */
    private void showTip(final String str) {
        mToast.setText(str);
        mToast.show();
    }


    /***
     * 裁剪图片
     * @param activity Activity
     * @param uri 图片的Uri
     */
    public void cropPicture(Activity activity, Uri uri) {
        Intent innerIntent = new Intent("com.android.camera.action.CROP");
        innerIntent.setDataAndType(uri, "image/*");
        innerIntent.putExtra("crop", "true");// 才能出剪辑的小方框,不然没有剪辑功能,只能选取图片
        innerIntent.putExtra("aspectX", 1); // 放大缩小比例的X
        innerIntent.putExtra("aspectY", 1);// 放大缩小比例的X   这里的比例为:   1:1
        innerIntent.putExtra("outputX", 320);  //这个是限制输出图片大小
        innerIntent.putExtra("outputY", 320);
        innerIntent.putExtra("return-data", true);
        // 切图大小不足输出,无黑框
        innerIntent.putExtra("scale", true);
        innerIntent.putExtra("scaleUpIfNeeded", true);
        innerIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(getImagePath())));
        innerIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        activity.startActivityForResult(innerIntent, REQUEST_INTENT_CROP);
    }

    /**
     * 设置保存图片路径
     * @return
     */
    private String getImagePath(){
        String path;
        if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return null;
        }
        path =  Environment.getExternalStorageDirectory().getAbsolutePath() +"/MFVDemo/";
        File folder = new File(path);
        if (folder != null && !folder.exists()) {
            folder.mkdirs();
        }
        path += "mfvtest.jpg";
        return path;
    }

    /**
     * 保存Bitmap至本地
     * @param bmp
     */
    private void saveBitmapToFile(Bitmap bmp){
        String file_path = getImagePath();
        File file = new File(file_path);
        FileOutputStream fOut;
        try {
            fOut = new FileOutputStream(file);
            bmp.compress(Bitmap.CompressFormat.JPEG, 85, fOut);
            fOut.flush();
            fOut.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void dismissProDialog() {
        if (null != mProDialog) {
            mProDialog.dismiss();
        }
    }
    private void update(Context con, String stuNum , int signFlag_add)
    {
        dbHelper = new DBHelper(con, "rollcall.db");
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        db.execSQL("update student set signFlag=? where stunum=?", new Object[]{signFlag_add,stuNum});
        db.close();
        dbHelper.close();
    }
    private List<StudentBase> query_name(Context con, String stuNum)
    {
        List<StudentBase> stu = new ArrayList<StudentBase>();
        dbHelper = new DBHelper(con, "rollcall.db");
        database = dbHelper.getReadableDatabase();
        // 去除重复的班级列表
        String ss = "select * from student where stunum = '" + stuNum + "'";
        Cursor cursor1 = database.rawQuery(ss, null);
        while (cursor1.moveToNext())
        {
            stu.add(new StudentBase(cursor1.getInt(cursor1.getColumnIndex("_id")),
                    cursor1.getString(cursor1.getColumnIndex("name")),
                    cursor1.getString(cursor1.getColumnIndex("stunum")),
                    cursor1.getString(cursor1.getColumnIndex("coursename")),
                    cursor1.getInt(cursor1.getColumnIndex("signFlag")),
                    cursor1.getInt(cursor1.getColumnIndex("leaveFlag")),
                    cursor1.getInt(cursor1.getColumnIndex("truantFlag")),
                    cursor1.getInt(cursor1.getColumnIndex("countnum"))));
        }
        database.close();
        cursor1.close();
        dbHelper.close();
        return stu;
    }
    /**
     * 三个函数留个纪念,弄了一下午,没想到啊,竟然是数据的问题!!!数据溢出了吗?
     */
//    public StudentBase findBynum(Context con,String stunum)
//    {
//        dbHelper = new DBHelper(con, "rollcall.db");
//        SQLiteDatabase db=dbHelper.getReadableDatabase();
//        String[] columns={"stunum","name","signFlag"};
//        String[] selectionArgs = {stunum};
//        Cursor c= db.query(true,"student",columns,"Name = ?",selectionArgs,null,null,null,null);
//        StudentBase dog=null;
//        if(c.moveToNext())
//        {
//            dog=new StudentBase();
//            dog.setStuNum(c.getString(c.getColumnIndexOrThrow("stunum")));
//            dog.setSignFlag(c.getInt(c.getColumnIndexOrThrow("signFlag")));
//            dog.setName(c.getString(c.getColumnIndexOrThrow("name")));
//        }
//        c.close();
//        db.close();
//        return dog;
//    }
//    private void LoadContent(Context con,String course)
//    {
//        BmobQuery<Student> query =new BmobQuery<>();
//        query.addWhereEqualTo("stuNum", "s1005");
//        query.findObjects(con, new FindListener<Student>() {
//            @Override
//            public void onSuccess(List<Student> list) {
//                showTip(list.get(0).getName()+"签到成功");
//            }
//
//            @Override
//            public void onError(int i, String s) {
//                showTip("下载失败");
//            }
//        });
//    }
}

activity_face_identify

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="#ebebeb">

    <com.example.rollcall.Topbar
        android:id="@+id/topbar_face"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#000"
        app:leftBackground="@drawable/leftselecter"
        app:titleText="人脸签到"
        app:titleTextColor="#FFF"
        app:titleTextSize="10sp"
        tools:layout_editor_absoluteX="8dp"
        tools:layout_editor_absoluteY="0dp" />

    <LinearLayout style="@style/Register_Input"
        android:layout_marginTop="60dp"
        android:id="@+id/linearLayout_meeting"
        android:layout_below="@+id/topbar_face"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true">

        <TextView
            android:id="@+id/textView4"
            style="@style/Black"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center_vertical"
            android:paddingLeft="10dp"
            android:text="会议ID:"
            android:textSize="18dp" />

        <EditText
            android:id="@+id/et_meeting"
            style="@style/Register_Edit"
            android:hint="请输入您的会议ID号"
            android:textColor="@color/gray" />

    </LinearLayout>

    <ImageView
        android:id="@+id/online_img"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:padding="4dp"
        android:src="@drawable/no_photo"
        android:layout_marginTop="25dp"
        android:layout_below="@+id/linearLayout_meeting"
        android:layout_centerHorizontal="true" />
    <Button
        android:id="@+id/button_face"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击扫脸"
        android:textSize="30dp"
        android:layout_below="@+id/online_img"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="18dp" />
    <Button
        android:id="@+id/button_face_rollcall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="确认签到"
        android:textSize="30dp"
        android:layout_marginTop="16dp"
        android:layout_below="@+id/button_face"
        android:layout_alignLeft="@+id/button_face"
        android:layout_alignStart="@+id/button_face" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/linearLayout_meeting"
        android:layout_alignStart="@+id/linearLayout_meeting"
        android:layout_below="@+id/button_face_rollcall"
        android:layout_marginTop="50dp"
        android:textColor="@color/gray"
        android:text="        由于技术原因,“到了没”人脸签到目前只能做到1:N人脸对比。故只能应用于人员到场较为松散的会议签到场景,您只需要输入'到了没'官方分配给您的会议ID即可进行刷脸签到。"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />


</RelativeLayout>

在工程 AndroidManifest.xml 文件中添加如下权限

<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--读取联系人权限,上传联系人需要用到此权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用人脸识别,还要添加:摄相头权限,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />

注意:
jdk中的so文件,必须要放到名为jniLibs的文件夹下:
这里写图片描述

只要认真看相关的开发文档和demo,应该不成问题。
给“到了没”添加完人脸签到的功能以后,APP也就基本成型了。
很开心!
马上过年了,博友们新年快乐!

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜鹊先生Richard

随缘~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值