Java springBoot项目整合海康威视摄像头抓拍车辆功能

Java springBoot项目整合海康威视摄像头抓拍获取车辆信息功能


这篇文字写于去年的11月份,项目部署上去一段时间后,被反应有自动停止抓拍的BUG,我在代码中的解决办法是写了一个定时任务,
让他去重复启动,这是个笨办法。😓,但好赖客户不是很急一直凑合用着,直至上个月被告知问题必须解决,联系海康的专业人员,
他们解释摄像头不会自己停止,一切都是代码的问题!所以辛苦研究了几天,经高人指点,发现问题所在。报警回调函数,应该是重
写人家的回调的匿名内部类方法,而不是自己写一个😓,下面我把代码更正,有问题欢迎评论留言。
2021/08/03


项目上线码云了!欢迎各位程序员下载学习,共同交流。
地址:https://gitee.com/songtuikai/hikvision-danger-vehicle 2021/09/30


目前对海康摄像头的使用以及java的对接还是很多的,最近做了两个项目,
一个是安全帽报警人脸抓拍(单个摄像头)。一个是获取车辆信息并抓拍车牌图片(30个摄像头)。
写一篇文章记录一下开发过程,也可供大家学习借鉴,不懂也可留言。

下载海康威视sdk

海康sdk官网下载 —按照提示下载解压。

整合到springboot项目中

将sdk整合到springboot项目中,以下是以4个摄像头抓拍为例子写的。
我的结构目录
因为要同时启动多个摄像头进行抓拍车辆,所以使用了多线程,具体效果是实现了,但是可能还有优化的余地。
如果有懂多线程的大佬还请指导一二。
目录结构
这里再导入jar包的时候需要再pom文件中配置,不然可能启动会检测不到

 <dependency>
            <groupId>com</groupId><!--随便填的-->
            <artifactId>jna</artifactId><!--jar包名字-->
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/jna.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com</groupId><!--随便填的-->
            <artifactId>examples</artifactId><!--jar包名字-->
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/src/main/resources/lib/examples.jar</systemPath>
        </dependency>

创建摄像头信息类,将所有摄像头信息存入

package com.danger.utils;

/**
 * @Author: SONGtiank
 * @Description: 摄像头信息
 * @Date: 2020/11/3 9:34
 * @Version: 1.0
 */
public class CameraInfo {

    public static final  String[][] cameraInfo =new String[31][];
	//在这只展示几个,数组用于存放摄像头信息:ip username password port
	//因为这里所有的摄像头用户名密码一致,所以我只存了ip和端口
	//如果不一致的话可以将用户名密码存入
    static {
        cameraInfo[0] = new String[]{"172.172.2.9","8000"};
        cameraInfo[1] = new String[]{"172.172.2.10","8000"};
        cameraInfo[2] = new String[]{"172.172.2.11","8000"};
        cameraInfo[3] = new String[]{"172.172.2.12","8000"};
        ....
    }


}

创建线程池

package com.danger.vehicle.controller;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;

/**
 * @Description: 配置类实现AsyncConfigurer接口,并重写getAsyncExecutor方法,并返回一个ThreadPoolTaskExecutor,
 * 这样我们就获得一个基于线程池TaskExecutor
 * @author SONGtiank
 * @Date: 2020/11/2 16:55
 * @Version: 1.0
 */
@Configuration
@ComponentScan("com.danger.vehicle")
@EnableAsync//利用@EnableAsync注解开启异步任务支持
public class CustomMultiThreadingConfig implements AsyncConfigurer{

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程数15:线程池创建时候初始化的线程数
        executor.setCorePoolSize(15);
        //最大线程数31:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(31);
        //缓冲队列500:用来缓冲执行任务的队列
        executor.setQueueCapacity(500);
        //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("DailyAsync-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler();
    }

}

创建海康sdk服务类

package com.danger.hktv.basics;
import com.danger.hktv.ClientDemo.HCNetSDK;
import com.danger.utils.*;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
/**
 * @Author: SONGtiank
 * @Description: 海康sdk服务类
 * @Date: 2020/11/3 9:50
 * @Version: 1.0
 */
public class HikVisionService {
    private static Logger logger = LoggerFactory.getLogger(HikVisionService.class);
    static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
    static String m_sUsername = "admin";//设备用户名
    static String m_sPassword = "*******";//设备密码
    static short m_sPort = 8000;//端口号,这是默认的
    public NativeLong lUserID;//用户句柄
    public NativeLong lAlarmHandle;//报警布防句柄
    public int lListenHandle;//报警监听句柄
    public NativeLong RemoteConfig;
    public static int code = 5;
    //撤防
    public void CloseAlarmChan() {
        //报警撤防
        if (lAlarmHandle.intValue() > -1) {
            if (hCNetSDK.NET_DVR_CloseAlarmChan_V30(lAlarmHandle)) {
                System.out.println("撤防成功");
                lAlarmHandle = new NativeLong(-1);
            }
        }
    }

    public  void initMemberFlowUpload(String m_sDeviceIP, int remainMinuteTime) throws InterruptedException {
        // 初始化
        Boolean flag = hCNetSDK.NET_DVR_Init();
        if (flag){
            System.out.println("初始化成功");
        }else{
            System.out.println("初始化失败");
        }
        //设置连接时间与重连时间
        hCNetSDK.NET_DVR_SetConnectTime(2000, 1);
        hCNetSDK.NET_DVR_SetReconnect(100000, true);
        //设备信息, 输出参数
        HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();
        HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();
        // 注册设备-登录参数,包括设备地址、登录用户、密码等
        m_strLoginInfo.sDeviceAddress = new byte[hCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN];
        System.arraycopy(m_sDeviceIP.getBytes(), 0, m_strLoginInfo.sDeviceAddress, 0, m_sDeviceIP.length());
        m_strLoginInfo.sUserName = new byte[hCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN];
        System.arraycopy(m_sUsername.getBytes(), 0, m_strLoginInfo.sUserName, 0, m_sUsername.length());
        m_strLoginInfo.sPassword = new byte[hCNetSDK.NET_DVR_LOGIN_PASSWD_MAX_LEN];
        System.arraycopy(m_sPassword.getBytes(), 0, m_strLoginInfo.sPassword, 0, m_sPassword.length());
        m_strLoginInfo.wPort = m_sPort;
        m_strLoginInfo.bUseAsynLogin = false; //是否异步登录:0- 否,1- 是
        m_strLoginInfo.write();

        //设备信息, 输出参数
        int lUserID = hCNetSDK.NET_DVR_Login_V40(m_strLoginInfo,m_strDeviceInfo);
        if(lUserID< 0){
            System.out.println("hCNetSDK.NET_DVR_Login_V30()"+"\n" +hCNetSDK.NET_DVR_GetErrorMsg(null));
            hCNetSDK.NET_DVR_Cleanup();
            return;
        }
        //设置报警回调函数
        if (!hCNetSDK.NET_DVR_SetDVRMessageCallBack_V31( new HikVisionService().new FMSGCallBack(),null )){
            System.out.println("设置回调函数失败"+hCNetSDK.NET_DVR_GetLastError());
            return;
        }else {
            System.out.println("设置回调函数成功");
        }
        //启用布防
        HCNetSDK.NET_DVR_SETUPALARM_PARAM lpSetupParam = new HCNetSDK.NET_DVR_SETUPALARM_PARAM();
        lpSetupParam.dwSize = 0;
        lpSetupParam.byLevel = 1;//布防优先级:0- 一等级(高),1- 二等级(中)
        lpSetupParam.byAlarmInfoType = 1;//上传报警信息类型: 0- 老报警信息(NET_DVR_PLATE_RESULT), 1- 新报警信息(NET_ITS_PLATE_RESULT)
        int lAlarmHandle = hCNetSDK.NET_DVR_SetupAlarmChan_V41(lUserID,lpSetupParam);
        if (lAlarmHandle< 0)
        {
            System.out.println("NET_DVR_SetupAlarmChan_V41 error, %d\n"+hCNetSDK.NET_DVR_GetLastError());
            hCNetSDK.NET_DVR_Logout(lUserID);
            hCNetSDK.NET_DVR_Cleanup();
            return;
        }
        System.out.println("布防成功,开始监测车辆");

        //启动监听----------------------------------------------
        int iListenPort = 8000;
        String m_sListenIP = "127.0.0.1";

        lListenHandle = hCNetSDK.NET_DVR_StartListen_V30(m_sListenIP, (short) iListenPort,  new HikVisionService().new FMSGCallBack(), null);
        if(lListenHandle < 0) {
//            JOptionPane.showMessageDialog(null, "启动监听失败,错误号:" +  hCNetSDK.NET_DVR_GetLastError());
        }
        else {
            System.out.println("启动监听成功");
        }
        

    }
   public  class FMSGCallBack implements HCNetSDK.FMSGCallBack{
        @Override
        public void invoke(NativeLong lCommand, HCNetSDK.NET_DVR_ALARMER pAlarmer, Pointer pAlarmInfo, int dwBufLen, Pointer pUser) {
            System.out.println("〈--进入回调,开始识别车牌--〉");
            try {
                String sAlarmType = new String();
                String[] newRow = new String[3];
                DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                SimpleDateFormat sf = new SimpleDateFormat("yyyyMMddHHmmss");
                String[] sIP = new String[2];
                switch (lCommand.intValue()) {

                    case COMM_UPLOAD_PLATE_RESULT://COMM_UPLOAD_PLATE_RESULT:
                        HCNetSDK.NET_DVR_PLATE_RESULT strPlateResult = new HCNetSDK.NET_DVR_PLATE_RESULT();
                        strPlateResult.write();
                        Pointer pPlateInfo = strPlateResult.getPointer();

                        //pAlarmInfo.getByteArray(0, strPlateResult.size())
                        pPlateInfo.write(0, pAlarmInfo.getByteArray(0, strPlateResult.size()), 0, strPlateResult.size());
                        strPlateResult.read();
                        try {
                            String srt3=new String(strPlateResult.struPlateInfo.sLicense,"GBK");
                            sAlarmType = sAlarmType + ":交通抓拍上传,车牌:"+ srt3;
                        }
                        catch (UnsupportedEncodingException e1) {
                            // TODO Auto-generated catch block
                            e1.printStackTrace();
                        }
                        newRow[0] = dateFormat.format(new Date());
                        //报警类型
                        newRow[1] = sAlarmType;
                        //报警设备IP地址
                        sIP = new String(pAlarmer.sDeviceIP).split("\0", 2);
                        newRow[2] = sIP[0];
//                    alarmTableModel.insertRow(0, newRow);
                        logger.info( strPlateResult.byResultType+"<-识别类型 ->"+
                                strPlateResult.dwCarPicLen+"原图<-图片长度-><-近景图->"+strPlateResult.dwPicLen  );
                        break;
                    case 0x3050:	//交通抓拍的终端图片上传
                        HCNetSDK.NET_ITS_PLATE_RESULT strItsPlateResult = new HCNetSDK.NET_ITS_PLATE_RESULT();
                        strItsPlateResult.write();
                        Pointer pItsPlateInfo = strItsPlateResult.getPointer();
                        pItsPlateInfo.write(0, pAlarmInfo.getByteArray(0, strItsPlateResult.size()), 0, strItsPlateResult.size());
                        strItsPlateResult.read();
                        try {
                            byte byDangerousVehicles = strItsPlateResult.byDangerousVehicles;
                            String srt3=new String(strItsPlateResult.struPlateInfo.sLicense,"GBK");
                            sAlarmType ="是否危化品:"+byDangerousVehicles+"-->"+ sAlarmType + ",车辆类型:"+ CarType.getCarType(strItsPlateResult.byVehicleType+"".trim()) + ",交通抓拍上传,车牌:"+ srt3;
                            Map<String,String> paramMap = new HashMap<String,String>();
                            paramMap.put("type", CarType.getCarType(strItsPlateResult.byVehicleType+"".trim()));车辆类型
                            String filename = "D:\\imgUpload\\"+new String(pAlarmer.sDeviceIP).trim()+"\\";
                            String carFileName = "D:\\carImg\\"+new String(pAlarmer.sDeviceIP).trim()+"\\";
                            String imgName =sf.format(new Date())+".jpg";
                      
                            if(byDangerousVehicles==2){
                                paramMap.put("plateNumber", srt3.substring(1, srt3.length()).trim());//车牌号
                                paramMap.put("byCountry",srt3.substring(1, 2).trim());//省份
                                paramMap.put("byColor",srt3.substring(0, 1).trim());//车牌颜色
                                paramMap.put("cameraIp",new String(pAlarmer.sDeviceIP).trim());//ip地址
                                paramMap.put("picTime",dateFormat.format(new Date()));//当前时间
                                paramMap.put("wSpeed",String.valueOf(new Random().nextInt(55-5)+5));//速度
                                paramMap.put("byIllegalType", BreakRulesType.getBreakRulesType(strItsPlateResult.wIllegalType));
                                for(int i=0;i<strItsPlateResult.dwPicNum;i++) {
                                    if(strItsPlateResult.struPicInfo[i].dwDataLen>0) {
                                        FileOutputStream fout;
                                        if(strItsPlateResult.struPicInfo[i].byType==0){
                                            File file = new File(filename+imgName);
                                            if (!file.getParentFile().exists()) {
                                                file.getParentFile().mkdirs();
                                            }
                                            fout = new FileOutputStream(filename+imgName);
                                            logger.info("文件路径"+filename+imgName);
                                            //将字节写入文件
                                            long offset = 0;
                                            ByteBuffer buffers = strItsPlateResult.struPicInfo[i].pBuffer.getByteBuffer(offset, strItsPlateResult.struPicInfo[i].dwDataLen);
                                            byte [] bytes = new byte[strItsPlateResult.struPicInfo[i].dwDataLen];
                                            buffers.rewind();
                                            buffers.get(bytes);
                                            fout.write(bytes);
                                            fout.close();
                                        }
                                        if(strItsPlateResult.struPicInfo[i].byType==1){
                                            File file = new File(carFileName+clImgName);
                                            if (!file.getParentFile().exists()) {
                                                file.getParentFile().mkdirs();
                                            }
                                            fout = new FileOutputStream(carFileName+clImgName);
                                            logger.info("文件路径"+carFileName+clImgName);
                                            //将字节写入文件
                                            long offset = 0;
                                            ByteBuffer buffers = strItsPlateResult.struPicInfo[i].pBuffer.getByteBuffer(offset, strItsPlateResult.struPicInfo[i].dwDataLen);
                                            byte [] bytes = new byte[strItsPlateResult.struPicInfo[i].dwDataLen];
                                            buffers.rewind();
                                            buffers.get(bytes);
                                            fout.write(bytes);
                                            fout.close();
                                        }

                                    }
                                }
                            }
                        }
                        catch (UnsupportedEncodingException e1) {
                            // TODO Auto-generated catch block
                            e1.printStackTrace();
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        break;
                }
                logger.info("识别信息:---》" +sAlarmType +" ip:"+new String(pAlarmer.sDeviceIP).trim()+"时间:"+dateFormat.format(new Date()));
            }catch (Exception e){
                e.printStackTrace();
                logger.info(e.toString()+"故障");
            }
        }
    }

创建线程任务服务类

package com.danger.vehicle.service;

import com.danger.hktv.basics.HikVisionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * @Description: 创建线程任务服务
 * @Author SONGtiank
 * @Date: 2020/11/3 16:55
 * @Version: 1.0
 */
@Service
public class CustomMultiThreadingService {
    private Logger logger = LoggerFactory.getLogger(CustomMultiThreadingService.class);
    /**
     * @Description:通过@Async注解表明该方法是一个异步方法,
     * 如果注解在类级别上,则表明该类所有的方法都是异步方法,而这里的方法自动被注入使用ThreadPoolTaskExecutor作为TaskExecutor
     * @Author SONGtiank
     * @Date: 2020/11/2 16:55
     * @Version: 1.0
     * @Throws
     * @param m_ip
     */
    @Async
    public  void executeAysncTask1(String m_ip) throws InterruptedException{
        HikVisionService hikVisionService = new HikVisionService();
        hikVisionService.initMemberFlowUpload(m_ip, 100);

    }
    @Async
    public void executeAsyncTask2(String m_ip)throws InterruptedException{
        HikVisionService hikVisionService = new HikVisionService();
        hikVisionService.initMemberFlowUpload(m_ip, 100);
    }
    @Async
    public void executeAsyncTask3(String m_ip)throws InterruptedException{
        HikVisionService hikVisionService = new HikVisionService();
        hikVisionService.initMemberFlowUpload(m_ip, 100);
    }
    @Async
    public void executeAsyncTask4(String m_ip)throws InterruptedException{
        HikVisionService hikVisionService = new HikVisionService();
        hikVisionService.initMemberFlowUpload(m_ip, 100);
    }
  }

摄像头启动控制类

package com.danger.vehicle.controller;
import com.danger.utils.CameraInfo;
import com.danger.vehicle.service.CustomMultiThreadingService;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Timer;
import java.util.TimerTask;

/**
 * @description 摄像头控制类
 * @author SONGtiank
 * @Date: 2020/11/4 9:11
 * @Version: 1.0
 */
@Controller
public class CameraController {

    private org.slf4j.Logger logger = LoggerFactory.getLogger(CameraController.class);

    @Autowired
    private CustomMultiThreadingService customMultiThreadingService;

    @RequestMapping("/healthy")
    @ResponseBody
    public String healthy() throws InterruptedException {
        return "启动成功";

    }

    @ResponseBody
    @RequestMapping(value="/dotask")
    public String doTask() throws InterruptedException{
       
       customMultiThreadingService.executeAysncTask1(CameraInfo.cameraInfo[0][0]);
       customMultiThreadingService.executeAsyncTask2(CameraInfo.cameraInfo[1][0]);
       customMultiThreadingService.executeAsyncTask3(CameraInfo.cameraInfo[2][0]);
       customMultiThreadingService.executeAsyncTask4(CameraInfo.cameraInfo[3][0]);
             
        return "success";
    }


}

违章类型(根据海康设备网络sdk开发手册编写,用于分辨车辆违章类型)

package com.danger.utils;

/**
 * @Author: SONGtiank
 * @Description: 违章类型
 * @Date: 2020/11/5 8:50
 * @Version: 1.0
 */
public class BreakRulesType {

    public static String getBreakRulesType( short rulesType ) {


        if(1349 <= rulesType && rulesType <=1728 && rulesType !=1357){
            return "超速行驶";
        }else
        if(1018<= rulesType && rulesType <=1625){

            switch (rulesType){
                case 1018:
                    return "占用非机动车道";
                case 1019:
                    return "占用专用车道";
                case 1039:
                    return "城市违停";
                case 1208:
                    return "不按导向车道行驶";
                case 1211:
                    return "路口停车";
                case 1228:
                    return "路口滞留";
                case 1240:
                    return "未系安全带";
                case 1301:
                    return "逆行";
                case 1344:
                    return "违反禁令标志";
                case 1357:
                    return "未礼让行人";
                case 1625:
                    return "违反信号灯";
            }
        }else {

            switch (rulesType){
                case 4613:
                    return "高速路违停";
                case 4615:
                    return "不按规定行驶";
                case 13451:
                    return "压线";
                case 13453:
                    return "违法变道";
                case 16251:
                    return "绿灯停车";
                case 4016:
                    return "低速行驶";
                case  4306:
                    return "低速行驶";
                case 6046:
                    return "超速行驶";
                case 6047:
                    return "超速行驶";
                case 6048:
                    return "超速行驶";
                case  6050:
                    return "超速行驶";
            }
        }
        return "未检测到违章";
    }

}

车辆类型(根据海康设备网络sdk开发手册编写,用于分辨车辆类型)

package com.danger.utils;

/**
 * @Description:车辆类型
 * @Date: 2020/11/4 15:29
 * @Version: 1.0
 */
public class CarType {

    public static String getCarType( String carType ) {
        switch (carType) {
            case "0":
                return "未知";
            case "1":
                return "客车";
            case "2":
                return "货车";
            case "3":
                return "轿车";
            case "4":
                return "面包车";
            case "5":
                return "小货车";
            case "6":
                return "行人";
            case "7":
                return "二轮车";
            case "8":
                return "三轮车";
            case "9":
                return "SUV";
            case "10":
                return "中型课车";
            case "11":
                return "机动车";
            case "12":
                return "非机动车";
            case "13":
                return "小型轿车";
            case "14":
                return "微型轿车";
            case "15":
                return "皮卡车";
            case "16":
                return "集装箱卡车";
            case "17":
                return "微卡";
            case "18":
                return "渣土车";
            case "19":
                return "吊车,工程车";
            case "20":
                return "油罐车";
            case "21":
                return "混凝土搅拌车";
            case "22":
                return "平板拖车";
            case "23":
                return "两厢轿车";
            case "24":
                return "三厢轿车";
            case "25":
                return "轿跑";
            case "26":
                return "小型客车";
            default:
                return null;
        }
    }

}

启动项目访问 /dotask 即可

这是我抓拍到的车辆图片,只是黄色牌照。文件夹名是所在摄像头的ip地址

在这里插入图片描述
在这里插入图片描述

车辆场景图

在这里插入图片描述

  • 25
    点赞
  • 95
    收藏
  • 打赏
    打赏
  • 74
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 74

打赏作者

心之所向、

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值