网络会议openmeetings下的openmeetings-util文件分析7

2021SC@SDUSC

上篇文章分析了main下的:ConnectionProperties类、NullStringer类、OMContextListener类、OmException类、Version类。接下来将继续分析main下的类,还剩下OmFileHelper类、OpenmeetingsVariables类、StoredFile类、XmlExport类,则openmeetings-util包分析完毕。

OmFileHelper类

public class OmFileHelper {
   private static File omHome = null;
   private static final String UPLOAD_DIR = "upload";
   private static final String PUBLIC_DIR = "public";
   private static final String CLIPARTS_DIR = "cliparts";
   private static final String WEB_INF_DIR = "WEB-INF";
   private static final String GROUP_LOGO_DIR = "grouplogo";
   private static final String STREAMS_DIR = "streams";
   private static final String EMOTIONS_DIR = "emoticons";
   private static final String LANGUAGES_DIR = "languages";
   private static final String CONF_DIR = "conf";
   private static final String IMAGES_DIR = "images";
   private static final String WML_DIR = "stored";

   public static final String FILE_NAME_FMT = "%s.%s";
   public static final String BACKUP_DIR = "backup";
   public static final String IMPORT_DIR = "import";
   public static final String PROFILES_DIR = "profiles";
   public static final String SCREENSHARING_DIR = "screensharing";
   public static final String CSS_DIR = "css";
   public static final String CUSTOM_CSS = "custom.css";
   public static final String FILES_DIR = "files";
   public static final String HIBERNATE = "hibernate";
   public static final String PERSISTENCE_NAME = "classes/META-INF/persistence.xml";
   public static final String DB_PERSISTENCE_NAME = "classes/META-INF/%s_persistence.xml";
   public static final String PROFILES_PREFIX = "profile_";
   public static final String LANG_FILE_NAME = "languages.xml";
   public static final String LIBRARY_FILE_NAME = "library.xml";
   public static final String PROFILE_IMG_NAME = "profile.png";
   public static final String PROFILE_FILE_NAME = "profile";
   public static final String RECORDING_FILE_NAME = "flvRecording_";
   public static final String THUMB_IMG_PREFIX = "_thumb_";
   public static final String DOC_PAGE_PREFIX = "page";
   public static final String TEST_SETUP_PREFIX = "TEST_SETUP_";
   public static final String DASHBOARD_FILE = "dashboard.xml";
   public static final String EXTENSION_WML = "wml";
   public static final String EXTENSION_FLV = "flv";
   public static final String EXTENSION_MP4 = "mp4";
   public static final String EXTENSION_JPG = "jpg";
   public static final String EXTENSION_PNG = "png";
   public static final String EXTENSION_PDF = "pdf";
   public static final String WB_VIDEO_FILE_PREFIX = "UPLOADFLV_";
   public static final String MP4_MIME_TYPE = "video/" + EXTENSION_MP4;
   public static final String JPG_MIME_TYPE = "image/jpeg";
   public static final String PNG_MIME_TYPE = "image/png";
   public static final String BCKP_ROOM_FILES = "roomFiles";
   public static final String BCKP_RECORD_FILES = "recordingFiles";

   //Sip related stuff
   public static final Long SIP_USER_ID = -1L;
   public static final String SIP_PICTURE_URI = "phone.png";

   private OmFileHelper() {}

    private static File getDir(File parent, String name) {
      File f = new File(parent, name);
      if (!f.exists()) {
         f.mkdirs();
      }
      return f;
   }
   public static File getUserProfilePicture(Long userId, String uri) {
      return getUserProfilePicture(userId, uri, getDefaultProfilePicture());
   }

   public static File getUserProfilePicture(Long userId, String uri, File def) {
      File img = null;
      if (SIP_USER_ID.equals(userId)) {
         img = new File(getImagesDir(), SIP_PICTURE_URI);
      } else if (userId != null) {
         img = new File(getUploadProfilesUserDir(userId), uri == null ? "" : uri);
      }
      if (img == null || !img.exists() || img.isDirectory()) {
         img = def;
      }
      return img;
   }
   public static File appendSuffix(File original, String suffix) {
      File parent = original.getParentFile();
      String name = original.getName();
      String ext = "";
      int idx = name.lastIndexOf('.');
      if (idx > -1) {
         name = name.substring(0, idx);
         ext = name.substring(idx);
      }
      return new File(parent, name + suffix + ext);
   }

   public static File getNewFile(File dir, String name, String ext) throws IOException {
      File f = new File(dir, getName(name, ext));
      int recursiveNumber = 0;
      while (f.exists()) {
         f = new File(dir, name + "_" + (recursiveNumber++) + ext);
      }
      f.createNewFile();
      return f;
   }

   public static File getNewDir(File dir, String name) throws IOException {
      File f = new File(dir, name);
      String baseName = f.getCanonicalPath();

      int recursiveNumber = 0;
      while (f.exists()) {
         f = new File(baseName + "_" + (recursiveNumber++));
      }
      f.mkdir();
      return f;
   }

   public static String getHumanSize(File dir) {
      return getHumanSize(getSize(dir));
   }

   public static String getHumanSize(long size) {
      if (size <= 0) {
         return "0";
      }
      final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" };
      int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
      return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
   }

   public static long getSize(File dir) {
      long size = 0;
      if (dir.isFile()) {
         size = dir.length();
      } else {
         for (File file : dir.listFiles()) {
            if (file.isFile()) {
               size += file.length();
            } else {
               size += getSize(file);
            }
         }
      }
      return size;
   }

   public static String getFileName(String name) {
      int dotidx = name.lastIndexOf('.');
      return dotidx < 0 ? "" : name.substring(0, dotidx);
   }

   public static String getFileExt(String name) {
      int dotidx = name.lastIndexOf('.');
      return dotidx < 0 ? "" : name.substring(dotidx + 1).toLowerCase(Locale.ROOT);
   }
}

该类是一个文件工具类,这个类的变量需要指向openmeetings webapp目录,该类定义的静态不变字符串常量用来指向openmeetings-web文件下src/main/webapp文件下的各个子目录。每个字符串变量均用来代表一个文件目录或者一个文件。

该类下面的get方法均利用getDir()方法根据上面定义的对应的变量来获取文件目录或文件并返回该File对象,便没有展示在代码中,如:

public static File getUploadFilesDir() {
   return getDir(getUploadDir(), FILES_DIR);
}

public static File getUploadProfilesDir() {
   return getDir(getUploadDir(), PROFILES_DIR);
}

 File appendSuffix()方法:为文件添加后缀,并返回。

File getNewFile()根据传入的参数:目录,文件名,文件后缀名(可没有该参数)来递归的创建一个新的文件或者目录并返回。

该类即是一个文件操作类,指向webapp下的目录并进行获取、创建操作,利用该类辅助其他文件类动态的对webapp下的文件进行操作和修改。

OpenmeetingsVariables类

public class OpenmeetingsVariables {
   public static final String ATTR_CLASS = "class";
   public static final String ATTR_TITLE = "title";
   public static final String PARAM_USER_ID = "userId";
   public static final String PARAM_STATUS = "status";
   public static final String PARAM_SRC = "src";
   public static final String PARAM__SRC = "_src";
   public static final String CONFIG_CRYPT = "crypt.class.name";
   public static final String CONFIG_DASHBOARD_SHOW_CHAT = "dashboard.show.chat";
   public static final String CONFIG_DASHBOARD_SHOW_MYROOMS = "dashboard.show.myrooms";
   public static final String CONFIG_DASHBOARD_SHOW_RSS = "dashboard.show.rssfeed";
   public static final String CONFIG_DASHBOARD_RSS_FEED1 = "dashboard.rss.feed1";
   public static final String CONFIG_DASHBOARD_RSS_FEED2 = "dashboard.rss.feed2";
   public static final String CONFIG_DEFAULT_LANG = "default.lang.id";
   public static final String CONFIG_DEFAULT_LANDING_ZONE = "default.landing.zone";
   public static final String CONFIG_DEFAULT_LDAP_ID = "default.ldap.id";
   public static final String CONFIG_DEFAULT_GROUP_ID = "default.group.id";
   public static final String CONFIG_DEFAULT_TIMEZONE = "default.timezone";
   public static final String CONFIG_REGISTER_FRONTEND = "allow.frontend.register";
   public static final String CONFIG_REGISTER_SOAP = "allow.soap.register";
   public static final String CONFIG_REGISTER_OAUTH = "allow.oauth.register";
   public static final String CONFIG_MAX_UPLOAD_SIZE = "max.upload.size";
   public static final String CONFIG_SIP_ENABLED = "sip.enable";
   public static final String CONFIG_SIP_ROOM_PREFIX = "sip.room.prefix";
   public static final String CONFIG_SIP_EXTEN_CONTEXT = "sip.exten.context";
   public static final String CONFIG_LOGIN_MIN_LENGTH = "user.login.minimum.length";
   public static final String CONFIG_PASS_MIN_LENGTH = "user.pass.minimum.length";
   public static final String CONFIG_IGNORE_BAD_SSL = "oauth2.ignore.bad.ssl";
   public static final String CONFIG_REDIRECT_URL_FOR_EXTERNAL = "redirect.url.for.external.users";
   public static final String CONFIG_APPOINTMENT_REMINDER_MINUTES = "number.minutes.reminder.send";
   public static final String CONFIG_APPLICATION_NAME = "application.name";
   public static final String CONFIG_APPLICATION_BASE_URL = "application.base.url";
   public static final String CONFIG_SCREENSHARING_QUALITY = "screensharing.default.quality";
   public static final String CONFIG_SCREENSHARING_FPS = "screensharing.default.fps";
   public static final String CONFIG_SCREENSHARING_FPS_SHOW = "screensharing.fps.show";
   public static final String CONFIG_SCREENSHARING_ALLOW_REMOTE = "screensharing.allow.remote";
   public static final String CONFIG_GOOGLE_ANALYTICS_CODE = "google.analytics.code";
   public static final String CONFIG_SMTP_SERVER = "mail.smtp.server";
   public static final String CONFIG_SMTP_PORT = "mail.smtp.port";
   public static final String CONFIG_SMTP_USER = "mail.smtp.user";
   public static final String CONFIG_SMTP_PASS = "mail.smtp.pass";
   public static final String CONFIG_SMTP_SYSTEM_EMAIL = "mail.smtp.system.email";
   public static final String CONFIG_SMTP_TLS = "mail.smtp.starttls.enable";
   public static final String CONFIG_SMTP_TIMEOUT_CON = "mail.smtp.connection.timeout";
   public static final String CONFIG_SMTP_TIMEOUT = "mail.smtp.timeout";
   public static final String CONFIG_PATH_IMAGEMAGIC = "path.imagemagick";
   public static final String CONFIG_PATH_SOX = "path.sox";
   public static final String CONFIG_PATH_FFMPEG = "path.ffmpeg";
   public static final String CONFIG_PATH_OFFICE = "path.office";
   public static final String CONFIG_DOCUMENT_DPI = "document.dpi";
   public static final String CONFIG_DOCUMENT_QUALITY = "document.quality";
   public static final String CONFIG_FLASH_SECURE = "flash.secure";
   public static final String CONFIG_FLASH_SECURE_PROXY = "flash.secure.proxy";
   public static final String CONFIG_FLASH_VIDEO_CODEC = "flash.video.codec";
   public static final String CONFIG_FLASH_VIDEO_FPS = "flash.video.fps";
   public static final String CONFIG_FLASH_VIDEO_BANDWIDTH = "flash.video.bandwidth";
   public static final String CONFIG_FLASH_CAM_QUALITY = "flash.cam.quality";
   public static final String CONFIG_FLASH_MIC_RATE = "flash.mic.rate";
   public static final String CONFIG_FLASH_ECHO_PATH = "flash.echoPath";
   public static final String CONFIG_HEADER_XFRAME = "header.x.frame.options";
   public static final String CONFIG_EXT_PROCESS_TTL = "external.process.ttl";
   public static final String CONFIG_HEADER_CSP = "header.content.security.policy";
   public static final String CONFIG_EMAIL_AT_REGISTER = "send.email.at.register";
   public static final String CONFIG_EMAIL_VERIFICATION = "send.email.with.verfication";
   public static final String CONFIG_CALENDAR_ROOM_CAPACITY = "calendar.conference.rooms.default.size";
   public static final String CONFIG_REPLY_TO_ORGANIZER = "inviter.email.as.replyto";
   public static final String CONFIG_KEYCODE_ARRANGE = "video.arrange.keycode";
   public static final String CONFIG_KEYCODE_EXCLUSIVE = "exclusive.audio.keycode";
   public static final String CONFIG_KEYCODE_MUTE = "mute.keycode";
   public static final String CONFIG_MYROOMS_ENABLED = "personal.rooms.enabled";
   public static final String CONFIG_REMINDER_MESSAGE = "reminder.message";
   public static final String CONFIG_MP4_AUDIO_RATE = "mp4.audio.rate";
   public static final String CONFIG_MP4_AUDIO_BITRATE = "mp4.audio.bitrate";
   public static final String CONFIG_MP4_VIDEO_PRESET = "mp4.video.preset";
   public static final String CONFIG_REST_ALLOW_ORIGIN = "rest.allow.origin";
   public static final String CONFIG_FNAME_MIN_LENGTH = "user.fname.minimum.length";
   public static final String CONFIG_LNAME_MIN_LENGTH = "user.lname.minimum.length";
   public static final String CONFIG_CHAT_SEND_ON_ENTER = "chat.send.on.enter";
   public static final String CONFIG_DISPLAY_NAME_EDITABLE = "display.name.editable";

   public static final String HEADER_XFRAME_SAMEORIGIN = "SAMEORIGIN";
   public static final String HEADER_CSP_SELF = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:;";
   public static final int RECENT_ROOMS_COUNT = 5;
   public static final int USER_LOGIN_MINIMUM_LENGTH = 4;
   public static final int USER_PASSWORD_MINIMUM_LENGTH = 8;
   public static final String DEFAULT_APP_NAME = "OpenMeetings";
   public static final String DEFAULT_CONTEXT_NAME = "openmeetings";
   public static final long DEFAULT_MAX_UPLOAD_SIZE = 100 * 1024 * 1024L; // 100MB
   public static final String FLASH_SECURE = "secure";
   public static final String FLASH_NATIVE_SSL = "native";
   public static final String FLASH_PORT = "rtmpPort";
   public static final String FLASH_SSL_PORT = "rtmpsPort";
   public static final String FLASH_VIDEO_CODEC = "videoCodec";
   public static final String FLASH_FPS = "fps";
   public static final String FLASH_BANDWIDTH = "bandwidth";
   public static final String FLASH_QUALITY = "quality";
   public static final String FLASH_ECHO_PATH = "echoPath";
   public static final String FLASH_MIC_RATE = "micRate";
   public static final int DEFAULT_MINUTES_REMINDER_SEND = 15;
   public static final String DEFAULT_BASE_URL = "http://localhost:5080/openmeetings/";
   public static final String DEFAULT_SIP_CONTEXT = "rooms";

   private static String webAppRootKey = null;
   private static String cryptClassName = null;
   private static String wicketApplicationName = null;
   private static String applicationName = null;
   private static int extProcessTtl = 20;
   private static int minLoginLength = USER_LOGIN_MINIMUM_LENGTH;
   private static int minPasswdLength = USER_PASSWORD_MINIMUM_LENGTH;
   private static JSONObject roomSettings = new JSONObject();
   private static boolean initComplete = false;
   private static long maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE;
   private static String baseUrl = DEFAULT_BASE_URL;
   private static boolean sipEnabled = false;
   private static String gaCode = null;
   private static Long defaultLang = 1L;
   private static Long defaultGroup = 1L;
   private static int audioRate = 22050;
   private static String audioBitrate = "32k";
   private static String videoPreset = "medium";
   private static String defaultTimezone = "Europe/Berlin";
   private static String restAllowOrigin = null;
   private static String sipContext = DEFAULT_SIP_CONTEXT;
   private static int minFnameLength = USER_LOGIN_MINIMUM_LENGTH;
   private static int minLnameLength = USER_LOGIN_MINIMUM_LENGTH;
   private static boolean chatSendOnEnter = false;
   private static boolean allowRegisterFrontend = false;
   private static boolean allowRegisterSoap = false;
   private static boolean allowRegisterOauth = false;
   private static boolean sendVerificationEmail = false;
   private static boolean sendRegisterEmail = false;
   private static boolean displayNameEditable = false;
}

 该类OpenmeetingsVariables定义了一系列openmeetings所需要的基本常变量,并为这些变量设置了get和set方法。如定义了屏幕共享默认的fps,SMTP的server,port,user,默认的app名字为OpenMeetings等等。大部分定义为public static可以直接供其它类来使用,private设置get和set方法来封装使用。

StoredFile类

public class StoredFile {
   private static final Logger log = Red5LoggerFactory.getLogger(StoredFile.class, getWebAppRootKey());
   private static final String MIME_AUDIO = "audio";
   private static final String MIME_VIDEO = "video";
   private static final String MIME_IMAGE = "image";
   private static final String MIME_TEXT = "text";
   private static final String MIME_APP = "application";
   private static final Set<MediaType> CONVERT_TYPES = new HashSet<>(Arrays.asList(
         application("x-tika-msoffice"), application("x-tika-ooxml"), application("msword")
         , application("vnd.wordperfect"), application("rtf")));

   private static final MediaType MIME_PNG = MediaType.parse(PNG_MIME_TYPE);
   private static final Set<MediaType> PDF_TYPES = new HashSet<>(Arrays.asList(application("pdf"), application("postscript")));
   private static final Set<MediaType> CHART_TYPES = new HashSet<>();
   private static final Set<MediaType> AS_IS_TYPES = new HashSet<>(Arrays.asList(MIME_PNG));
   private static final String ACCEPT_STRING;
   private static TikaConfig tika;
   static {
      Set<MediaType> types = new LinkedHashSet<>();
      types.addAll(CONVERT_TYPES);
      types.addAll(PDF_TYPES);
      //TODO Charts need to added
      StringBuilder sb = new StringBuilder("audio/*,video/*,image/*,text/*");
      sb.append(",application/vnd.oasis.opendocument.*");
      sb.append(",application/vnd.sun.xml.*");
      sb.append(",application/vnd.stardivision.*");
      sb.append(",application/x-star*");
      for (MediaType mt : types) {
         sb.append(',').append(mt.toString());
      }
      ACCEPT_STRING = sb.toString();
      try {
         tika = new TikaConfig();
      } catch (IOException | TikaException e) {
         log.error("Unexpected exception while initializing TIKA", e);
         throw new RuntimeException(e);
      }
   }

   private String name;
   private String ext;
   private MediaType mime;

   public StoredFile(String fullname, InputStream is) {
      this(fullname, null, is);
   }

   public StoredFile(String name, String ext, InputStream is) {
      init(name, ext, is);
   }

   public StoredFile(String fullname, File f) throws IOException {
      this(fullname, null, f);
   }

   public StoredFile(String name, String ext, File f) throws IOException {
      try (InputStream fis = new FileInputStream(f)) {
         init(name, ext, fis);
      }
   }

   private void init(String _name, String _ext, InputStream is) {
      if (Strings.isEmpty(_ext)) {
         int idx = _name.lastIndexOf('.');
         name = idx < 0 ? _name : _name.substring(0, idx);
         ext = getFileExt(_name);
      } else {
         name = _name;
         ext = _ext.toLowerCase(Locale.ROOT);
      }
      Metadata md = new Metadata();
      md.add(RESOURCE_NAME_KEY, String.format(FILE_NAME_FMT, name, ext));
      try {
         mime = tika.getDetector().detect(is == null ? null : TikaInputStream.get(is), md);
      } catch (Throwable e) {
         mime = null;
         log.error("Unexpected exception while detecting mime type", e);
      }
   }

   public static String getAcceptAttr() {
      return ACCEPT_STRING;
   }

   public boolean isOffice() {
      if (mime == null) {
         return false;
      }
      return MIME_TEXT.equals(mime.getType())
            || (MIME_APP.equals(mime.getType()) &&
                  (mime.getSubtype().startsWith("vnd.oasis.opendocument")
                     || mime.getSubtype().startsWith("vnd.sun.xml")
                     || mime.getSubtype().startsWith("vnd.stardivision")
                     || mime.getSubtype().startsWith("x-star")
                     || mime.getSubtype().startsWith("vnd.ms-")
                     || mime.getSubtype().startsWith("vnd.openxmlformats-officedocument")
               ))
            || CONVERT_TYPES.contains(mime);
   }
}

该类StoredFile存储文件,定义了Logger日志类对象log来设置和打印日志,定义了openmeetings的文件类型“audio”、“video”、“image”、“text”、“application”、“pdf”等等,为白板演示操作做准备,初始化TIKA配置,获取和存储初始化,利用InputStream输入流将上传的文件存储至缓冲区域,检测时出现异常则用log报error错误打印出来“Unexpected exception while detecting mime type”。

XmlExport类

public class XmlExport {
   public static final String FILE_COMMENT = ""
         + "\n"
         + "  Licensed to the Apache Software Foundation (ASF) under one\n"
         + "  or more contributor license agreements.  See the NOTICE file\n"
         + "  distributed with this work for additional information\n"
         + "  regarding copyright ownership.  The ASF licenses this file\n"
         + "  to you under the Apache License, Version 2.0 (the\n"
         + "  \"License\"); you may not use this file except in compliance\n"
         + "  with the License.  You may obtain a copy of the License at\n"
         + "\n"
         + "      http://www.apache.org/licenses/LICENSE-2.0\n"
         + "\n"
         + "  Unless required by applicable law or agreed to in writing,\n"
         + "  software distributed under the License is distributed on an\n"
         + "  \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n"
         + "  KIND, either express or implied.  See the License for the\n"
         + "  specific language governing permissions and limitations\n"
         + "  under the License.\n"
         + "\n"
         + "\n"
         + "###############################################\n"
         + "This File is auto-generated by the LanguageEditor\n"
         + "to add new Languages or modify/customize it use the LanguageEditor\n"
         + "see https://openmeetings.apache.org/LanguageEditor.html for Details\n"
         + "###############################################\n";

   private XmlExport() {}

   public static Document createDocument() {
      Document document = DocumentHelper.createDocument();
      document.setXMLEncoding(UTF_8.name());
      document.addComment(XmlExport.FILE_COMMENT);
      return document;
   }

   public static Element createRoot(Document document) {
      document.addDocType("properties", null, "http://java.sun.com/dtd/properties.dtd");
      return document.addElement("properties");
   }

   public static Element createRoot(Document document, String root) {
      return document.addElement(root);
   }

   public static void toXml(Writer out, Document doc) throws Exception {
      OutputFormat outformat = OutputFormat.createPrettyPrint();
      outformat.setIndentSize(1);
      outformat.setIndent("\t");
      outformat.setEncoding(UTF_8.name());
      XMLWriter writer = new XMLWriter(out, outformat);
      writer.write(doc);
      writer.flush();
      out.flush();
      out.close();
   }

   public static void toXml(File f, Document doc) throws Exception {
      toXml(new FileOutputStream(f), doc);
   }

   public static void toXml(OutputStream out, Document doc) throws Exception {
      toXml(new OutputStreamWriter(out, "UTF8"), doc);
   }
}

该类XmlExport主要是用来将该类下定义的常量FILE_COMMENT字符串输出到每个文件的文件开始,如下图这样,为每一个文件设置版权信息和使用许可。

有关版权所有权的更多信息,请参阅随本作品发布的NOTICE文件。ASF在Apache许可证2.0版(“许可证”)下许可这个文件给你,除非符合许可证,否则你不能使用这个文件。

总结

openmeetings-util下所有类已经分析完毕。该util文件下面的类为openmeetings的工具类,提供了许多种如日期工具类CalendarHelper.java、进程工具栏ProcessHelper.java、om文件工具类OmFileHelper.java等等封装供其他文件使用。截至到该篇文章,我所负责的openmeetings-screenshare文件和openmeetings-util文件已全部分析完毕。分配的任务已完成。希望还有机会继续分析和使用openmeetings这个web应用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值