Loading [MathJax]/jax/output/CommonHTML/jax.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >基于业务的列表比较器

基于业务的列表比较器

作者头像
叔牙
发布于 2020-11-19 06:47:35
发布于 2020-11-19 06:47:35
2.1K0
举报

在很多情况下前端页面或者其他客户端和后台交互提交数据都是单条数据的更新和插入,

但是在有些场景下,基于特定的业务客户端需要一列表的方式提交数据,我们传统的解决方案是讲苦中的数据删除,然后将客户端传来的数据列表批量插入,但是这样就有很多弊

弊端,1)有些数据根本没有变动,而经历了一此删除和插入,2)增加了数据库交互次数,删除和插入会带来数据锁定,从而带来额外的性能损耗。接下来我们将根据实际案例分析来实现将提交数据列表和库中数据对比来避免上述问题

背景

在crm2.0系统退费业务中,门店红娘主任发起退费申请,需要上传相应的pos小票,解除服务协议,委托书等图片信息,图片上传到资源服务器后会将信息存放到RefundInfoImg表

中通过退费id和退费单关联;此后需要门店财务审批和退费组核实操作,这两步骤操作人员都可以修改图片信息(发现错误后重新上传或者删除操作),审批提交的时候,前端提交

一个人imgs信息列表,如果是新上传图片没有id信息,如果是之前在库图片信息提交时会

存在id信息.

问题

列表提交到后台,一般的解决方案是将库中改退费id对应的图片信息删除,然后将前端提交的列表保存在数据库,但是增加了数据库交互次数并且存在性能问题.

解决方案

前端传来的图片列表信息在入库之前,和库中的数据对比分析得出哪些数据那要新增,哪些数据需要更新,哪些数据需要删除,然后在执行持久化操作

实现方式

在工程中需要添加一下包中的几个类:

1. IComparator:比较接口

2. AbstractComparator:对比抽象类,实现了一些通用操作,一些自定义操作使用末班方法交给子类去实现

3. CompareContext:对比上下文,也可以理解为一个容器,对比的数据都是从该类实例中获取

4. CompareRule:对比规则,使用者可以根据自身需要定义特定的比较规则

5. CompareResult:比较结果,比较完成后比较器会将结果(新增信息,更新信息,删除信息)放入此类实例返回

6. UserComparetor:这是一个自定义比较器,根据需要自己实现(该案例中我们比较用户信息)

下边贴出了各个类的代码实现

IComparator:

/**

* 执行比较的接口

*

* @author Typhoon

* @date 2017-09-17 10:20 Sunday

* @since V2.0

* @param <S>

* @param <T>

*/

public abstract interface IComparator<S,T> {

/**

* 执行比较操作

*

* @param paramCompareContext

* @return

*/

public abstract CompareResult<T> compare(CompareContext paramCompareContext);

/**

* 获取默认的对比规则

*

* @return

*/

public abstract CompareRule getDefaultRule();

/**

* 创建的时候执行一些初始化操作

*

* @param paramS

* @return

*/

public abstract T onCreate(S paramS);

/**

* 更新的时候执行初始化操作

*

* @param paramS

* @param paramT

*/

public abstract void onUpdate(S paramS, T paramT);

/**

* 删除的时候执行初始化操作

*

* @param paramT

*/

public abstract void onDelete(T paramT);

}

AbstractComparator:

/**

* 对比

*

* @author Typhoon

*

* @param <S>

* @param <T>

*/

public abstract class AbstractComparator<S, T> implements IComparator<S, T> {

private Logger logger = LoggerFactory.getLogger(AbstractComparator.class);

private CompareRule defaultRule;

public AbstractComparator() {

}

public CompareRule getDefaultRule() {

return this.defaultRule;

}

public void setDefaultRule(CompareRule defaultRule) {

this.defaultRule = defaultRule;

}

@SuppressWarnings({ "unchecked", "rawtypes" })

public CompareResult<T> compare(CompareContext context) {

CompareRule rule = (context.getRule() != null) ? context.getRule() : getDefaultRule();

Assert.notNull(rule, "CompareRule can't be null.");

Object source = context.getSource();

Object target = context.getTarget();

Assert.notNull(source, "Source can't be null.");

Assert.notNull(target, "Target can't be null.");

if ((source instanceof Collection) && (target instanceof Collection)) {// 如果都是集合类型,直接比较

return doCompare((Collection) source, (Collection) target, rule);

}

Collection sourceList;

if ((!(source instanceof Collection)) && (!(target instanceof Collection))) {// 如果都不是集合类型,转换成list比较

sourceList = new ArrayList(1);// 避免初始化过多内存空间

sourceList.add(source);

Collection targetList = new ArrayList();

targetList.add(target);

return doCompare(sourceList, targetList, rule);

}

if ((!(source instanceof Collection)) && (target instanceof Collection)) {

sourceList = new ArrayList(1);

sourceList.add(source);

return doCompare(sourceList, (Collection) target, rule);

}

if ((source instanceof Collection) && (!(target instanceof Collection))) {

Collection targetList = new ArrayList();

targetList.add(target);

return doCompare((Collection) source, targetList, rule);

}

throw new IllegalArgumentException(String.format("Not support compare %s vs %s", new Object[] { source, target }));

}

public void onUpdate(S source, T target) {

}

public void onDelete(T target) {

}

/**

* 真正的执行对比操作

*

* @author Typhoon

* @date 2017-09-17 10:25 Sunday

* @since V2.0

* @param source

* @param target

* @param rule

* @return

*/

@SuppressWarnings({ "rawtypes", "unchecked" })

protected CompareResult<T> doCompare(Collection<S> source, Collection<T> target, CompareRule rule) {

this.logger.info("comparing...");

long start = System.currentTimeMillis();

CompareResult<T> result = new CompareResult<>();

T tmpTarget;

Iterator i$;

if (null != source && !source.isEmpty()) {// 先遍历源数据,可以对比出需要增加和更新的内容

tmpTarget = null;

for (i=source.iterator();i.hasNext();) {

S s = (S) i$.next();

if (isNeedCreate(s, target, rule)) {// ①先检查是否有需要新增的

tmpTarget = onCreate(s);// 初始化一些属性

// 将源数据元素加入到新增列表

result.getNewList().add(tmpTarget);

} else if ((tmpTarget = getUpdateObject(s, target, rule)) != null) {// ②检查是否需要更新

// 有相等的元素

// 获取目标类的hashcode

int beforeUpdateHash = HashCodeBuilder.reflectionHashCode(tmpTarget, true);

if (rule.isAutoUpdate()) {// 如果需要自动更新值,直接将源数据值复制到目标类中

copyProperties(s, tmpTarget);

}

onUpdate(s, tmpTarget);// 触发更新的时候做额外一些业务,钩子方法

// 获取赋值后的目标数据hashcode,其实可以理解为源数据hashcode

int afterUpdateHash = HashCodeBuilder.reflectionHashCode(tmpTarget, true);

if (beforeUpdateHash != afterUpdateHash) {// 如果不一致,就放入需要更新的列表中

result.getUpdateList().add(tmpTarget);

}

}

}

}

// Iterator i$;

if ((target != null) && (target.size() > 0)) {

for (i=target.iterator();i.hasNext();) {

T t = (T) i$.next();

// 将目标列表元素和源数据列表对比,如果源数据中没有,说明该元素需要删除

if (isNeedDelete(source, t, rule)) {

onDelete(t);

result.getDeleteList().add(t);

}

}

}

long end = System.currentTimeMillis();

this.logger.info("complete..time-consuming : " + (end - start) + "ms");

return result;

}

/**

* 将源数据内容复制到目标数据中

*

* @param source

* @param target

*/

protected void copyProperties(S source, T target) {

try {

PropertyUtils.copyProperties(target, source);

} catch (Exception e) {

this.logger.error("Error occur:", e);

}

}

/**

* 根据对比规则和数据返回唯一结果

*

* @author Typhoon

* @date 2017-09-17 10:41 Sunday

* @since V2.0

* @param obj

* @param fields

* @param joinChar

* @param hash

* @return

*/

private String getCompareValue(Object obj, String[] fields, String joinChar, boolean hash) {

Assert.notNull(obj, "Object can't be null.");

Assert.notEmpty(fields, "Compare fields can't be empty.");

StringBuffer sb = new StringBuffer();

try {

// 用标记把value连起来

Object tmp = null;

for (String field : fields) {// 将对比规则中需要比较的属性和对应的值使用连接符号拼接起来,类似id_1

if ((joinChar != null) && (sb.length() > 0)) {

sb.append(joinChar);

}

tmp = PropertyUtils.getProperty(obj, field);

sb.append((tmp == null) ? "" : tmp.toString());

}

} catch (Exception e) {

this.logger.error("Error occur:", e);

}

String value = sb.toString();

return ((hash) ? Md5.getMD5ofStr(value) : value);// 将拼接结果转换成字符串后返回(唯一字符串)

}

/**

* 判断源数据和目标数据是否相等

* <p>

* 比较规则自定义

* </p>

*

* @author Typhoon

* @date 2017-09-17 10:34 Sunday

* @since V2.0

* @param source

* @param target

* @param rule

* @return

*/

private boolean equals(Object source, Object target, CompareRule rule) {

Assert.notNull(rule, "CompareRule can't be null.");

// 根据属性比较两个对象是否相等

String sValue = getCompareValue(source, rule.getSourceAttrs(), rule.getJoinChar(), rule.isHash());

String tValue = getCompareValue(target, rule.getTargetAttrs(), rule.getJoinChar(), rule.isHash());

return sValue.equals(tValue);

}

/**

* 由源数据单元素和目标列表对比,检查是否需要新增

*

* @author Typhoon

* @date 2017-09-17 10:29 Sunday

* @since V2.0

* @param s 源数据单个元素

* @param target 目标列表

* @param rule 比较规则

* @return

*/

private boolean isNeedCreate(S s, Collection<T> target, CompareRule rule) {

Iterator<T> i$;

if (null == target || target.isEmpty()) {// 目标没有数据,直接判定需要新增

return true;

}

for (i=target.iterator();i.hasNext();) {

Object t = i$.next();

if (equals(s, t, rule)) {// 如果找到目标列表中与源数据匹配的数据,说明改数据存在,不需要新增

return false;

}

}

return true;

}

/**

* 从源数据获取需要更新的元素

*

* @param s

* @param target

* @param rule

* @return

*/

private T getUpdateObject(S s, Collection<T> target, CompareRule rule) {

Iterator<T> i$;

if (null == target || target.isEmpty()) {

return null;

}

for (i=target.iterator();i.hasNext();) {

T t = i$.next();

if (equals(s, t, rule)) {// 如果有判定属性相等的内容,返回目标列表中的该元素

return t;

}

}

return null;

}

/**

* 检查是否需要删除

*

* @author Typhoon

* @date 2017-09-17 11:02 Sunday

* @since V2.0

* @param source

* @param t

* @param rule

* @return

*/

private boolean isNeedDelete(Collection<S> source, T t, CompareRule rule) {

Iterator<S> i$;

if (null == source || source.isEmpty()) {

return true;

}

for (i=source.iterator();i.hasNext();) {

Object s = i$.next();

if (equals(s, t, rule)) {

return false;

}

}

return true;

}

CompareContext:

/**

* 对比上下文(容器)

*

* @author Typhoon

* @date 2017-09-17 10:18 Sunday

* @since V2.0

*/

public class CompareContext {

/**

* 源数据

*/

private Object source;

/**

* 目标数据

*/

private Object target;

/**

* 对比规则

*/

private CompareRule rule;

public CompareContext() {

}

public CompareContext(Object source, Object target) {

this(source, target, null);

}

public CompareContext(Object source, Object target, CompareRule rule) {

this.source = source;

this.target = target;

this.rule = rule;

}

public Object getSource() {

return this.source;

}

public void setSource(Object source) {

this.source = source;

}

public Object getTarget() {

return this.target;

}

public void setTarget(Object target) {

this.target = target;

}

public CompareRule getRule() {

return this.rule;

}

public void setRule(CompareRule rule) {

this.rule = rule;

}

}

CompareRule:

/**

* 对比规则

*

* @author Typhoon

* @date 2017-09-17 11:07 Sunday

* @since V2.0

*/

public class CompareRule {

/**

* 源数据属性

*/

private String[] sourceAttrs;

/**

* 目标属性

*/

private String[] targetAttrs;

/**

* 连接字符

*/

private String joinChar;

/**

* 是否需要hash计算

*/

private boolean hash;

/**

* 是否自行更新

*/

private boolean autoUpdate;

public CompareRule() {

this.joinChar = "_";

}

public String[] getSourceAttrs() {

return this.sourceAttrs;

}

public void setSourceAttrs(String[] sourceAttrs) {

this.sourceAttrs = ((String[]) ArrayUtils.clone(sourceAttrs));

}

public String[] getTargetAttrs() {

return this.targetAttrs;

}

public void setTargetAttrs(String[] targetAttrs) {

this.targetAttrs = ((String[]) ArrayUtils.clone(targetAttrs));

}

public String getJoinChar() {

return this.joinChar;

}

public void setJoinChar(String joinChar) {

this.joinChar = joinChar;

}

public boolean isHash() {

return this.hash;

}

public void setHash(boolean hash) {

this.hash = hash;

}

public boolean isAutoUpdate() {

return this.autoUpdate;

}

public void setAutoUpdate(boolean autoUpdate) {

this.autoUpdate = autoUpdate;

}

}

CompareResult:

/**

* 对比结果容器

*

* @author Typhoon

* @date 2017-09-17 11:04 Sunday

* @since V2.0

* @param <T>

*/

public class CompareResult<T> {

/**

* 需要更新的数据列表

*/

private List<T> updateList;

/**

* 需要删除的数据列表

*/

private List<T> deleteList;

/**

* 需要新增的数据列表

*/

private List<T> newList;

public CompareResult() {

this.updateList = new ArrayList<>();

this.deleteList = new ArrayList<>();

this.newList = new ArrayList<>();

}

/**

* 总共需要改变的数量

*

* @author Typhoon

* @date 2017-09-17 11:05 Sunday

* @since V2.0

* @return

*/

public int getChangeCount() {

return (this.updateList.size() + this.deleteList.size() + this.newList.size());

}

public List<T> getUpdateList() {

return this.updateList;

}

public void setUpdateList(List<T> updateList) {

this.updateList = updateList;

}

public List<T> getDeleteList() {

return this.deleteList;

}

public void setDeleteList(List<T> deleteList) {

this.deleteList = deleteList;

}

public List<T> getNewList() {

return this.newList;

}

public void setNewList(List<T> newList) {

this.newList = newList;

}

}

UserComparator:

/**

* 用户信息比较器

*

* @author Typhoon

* @date 2017-09-17 11:12 Sunday

* @since V2.0

*/

public class UserComparetor extends AbstractComparator<User, User> {

@Override

public User onCreate(User paramS) {

paramS.setCreateTime(new Date());

return paramS;

}

}

下边是具体业务实现:

然后编写单元测试类模拟客户端提交:

/**

* 模拟用户信息对比客户端

*

* @author Typhoon

*

*/

public class UserCompareConsumer {

public static void main(String[] args) throws ParseException {

List<User> sourceList = new ArrayList<>(2);

User u = new User();

u.setName("aeolus");

sourceList.add(u);

u = new User();

u.setId(2L);

u.setName("typhoon");

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

u.setCreateTime(format.parse("2017-09-17 11:14:03"));

sourceList.add(u);

new ClassPathXmlApplicationContext("spring-root.xml").start();

UserService userService = SpringContextUtil.getBean("userService", UserService.class);

CompareResult<User> result = userService.compareUserList(sourceList);

System.out.println(JSON.toJSONString(result));

}

运行程序得到如下结果:

将结果格式化:

查看数据库中的目标数据如下:

对比分析,我们已经计算出了需要新增,更新和删除的数据,接下来自己实现响应的数据持久化操作就可以了

总结

这种方式是牺牲一定的java性能,来换取数据库操作的性能,从逻辑层面和性能层面都是划得来的。如果发现有瑕疵或者纰漏的地方,还请各位多多指正!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-09-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JavaSE(八)集合之List
前面一篇的corejava讲的是集合的概述,这一篇我将详细的和大家讲解一下Collection下面的List、set、queue这三个子接口。希望大家能得到提升。 一、List接口 1.1、List接口概述   List类型集合特点:集合中的元素有序且可重复,有下标 。     注:有序指的是元素放到集合中的顺序和循环遍历出来的顺序一致   List接口常见的实现类有:ArrayList、LinkedList、Vector等     对于数据的随机访问,ArrayList效率优于LinkedList,因为L
用户1195962
2018/01/18
7190
JavaSE(八)集合之List
手写一个orm框架-4
在上一篇里,我们已经取到了我们在生成sql语句中所需要的信息,这一篇里我们开始根据class来生成我们需要的sql。在这之前我们先确认几件事情
何白白
2019/06/28
5300
spring\spring boot拷贝实体的工具类---BeanObjectCopyUtils
介绍一个实用的bean对象实体类的拷贝工具,主要封装了两个方法进行实体类的字符拷贝处理,单个实体以及实体列表的拷贝操作。
小马哥学JAVA
2023/01/19
8440
Java工具集-复杂更新逻辑工具
简单工具类 写作初衷:由于日常开发经常需要用到很多工具类,经常根据需求自己写也比较麻烦 网上好了一些工具类例如commom.lang3或者hutool或者Jodd这样的开源工具,但是 发现他们之中虽然设计不错,但是如果我想要使用,就必须要引入依赖并且去维护依赖,有些 甚至会有存在版本编译不通过问题,故此想要写作一个每个类都可以作为独立工具类使用 每个使用者只需要复制该类,到任何项目当中都可以使用,所以需要尊从以下两个原则才能 做到.在此诚邀各位大佬参与.可以把各自用过的工具,整合成只依赖JDK
cwl_java
2019/10/26
5450
JDK 工具类之 Collections
/* * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package java.util; import java.io.Serializable; import java.io.ObjectOutputStream; import java.io.I
一个会写诗的程序员
2022/05/13
2860
基于jdbcTemplate实现物理分页
众所周知,在物联网世界里,我们大部分的操作是来自查询,我们面试经常被问到的QPS其实就是针对查询的,说到查询,根据实际的场景也一般分为单个查询和批量查询,例如:查询会员的详情信息是单个查询,查询会员列表就是典型的批量查询,说到批量查询那么每次查询的数量就要受限,DB单次查询量限制,网络传输带宽限制,应用程序接收数据量大小限制等等,那么这时候分页查询变得非常必要,每次查询出指定大小的单页数据,翻页的时候调整分页参数再次查询。
叔牙
2020/11/19
2.6K0
基于jdbcTemplate实现物理分页
java---集合(数据结构)(重点)
以前存储一组相同类型的数据使用数组,固定大小,具有连续性的存储空间。比如,5个长度的数组再存入数据时,如果现在已经存满,存入第六个元素,这时数组空间不够,扩容。Arrays.copyOf() , 很不方便,如果扩容频率太高,也影响你程序运行效率。集合来解决数组固定,如果扩容又影响效率的问题
用户10787181
2023/10/17
2530
java---集合(数据结构)(重点)
基于spring-jdbc中JdbcTemplate实现查询高可用
在传统系统或网站架构中,一般都是使用单点,当然单点在应对并发访问量不是很大的场景下是能够应对自如的,并且单机不存在服务延迟,分布式事务等问题,部署也比较简单。
叔牙
2020/11/19
1.1K0
基于spring-jdbc中JdbcTemplate实现查询高可用
Java8使用stream操作两个list根据某字段匹配再对其中一个list进行赋值
import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; @Slf4j public class ListUtils { /** * lambda表达式对两个List进行循环,根据符合条件,进行相关的赋值操作并
JavaEdge
2021/12/07
5K0
JAVA入门学习六
描述: 集合的由来数组长度是固定,当添加的元素超过了数组的长度时需要对数组重新定义太麻烦,java内部给我们提供了集合类能存储任意对象,长度是可以改变的,随着元素的增加而增加,随着元素的减少而减少;
全栈工程师修炼指南
2020/10/23
6100
JAVA入门学习六
AutoMapper快速上手
AutoMapper是一个简单的对象映射框架(OOM),对象映射原理是把一种类型的输入对象转换为不同类型的输出对象,通俗讲就是通过一些约束讲一种类型中数据自动映射到另一数据类型中
莫问今朝
2019/02/25
4.3K0
JDK 工具类之 Collections 2
/** * Returns a synchronized (thread-safe) map backed by the specified * map. In order to guarantee serial access, it is critical that * <strong>all</strong> access to the backing map is accomplished * through the returned map.<p>
一个会写诗的程序员
2022/05/13
3830
自定义注解实现参数验证与业务代码解耦
核心依赖 <!-- aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId>
Meet相识
2018/09/12
1.3K0
[搞懂Java集合类3]Iterator,fail-fast机制与比较器
迭代对于我们搞Java的来说绝对不陌生。我们常常使用JDK提供的迭代接口进行Java集合的迭代。
Java技术江湖
2019/09/25
7470
java解析任意层的json数据(递归解析) 原
/** * JSONObject解析方法(可以解析任意层json,采用递归解析的方法) * @param objJson * @param menu 父菜单实体类 * @param list List<Menu>集合 * @return */ @SuppressWarnings("rawtypes") public static List<Menu> analysisJson(Object objJson,Menu menu,List<Menu> list) { // 如果ob
wuweixiang
2018/08/14
3.3K0
springmvc统一异常拦截方式
背景描述 web项目开发过程中和前期线上运行环境,总是会或多或少出现各种异常, 比较常见的是有些运行异常或者检测异常直接抛给了前端,导致前端页面 出现了一大坨的异常堆栈信息。 1.但是站在产品和用户的角度,不管你服务器端出现什么异常, 或者说崩溃的东西,和我没关系; 2.站在B/S交互的角度,你Server端的运行错误与否和我 Browser端没太大关联,我任何一个操作, 只有成功和失败两种结果,不存在一直加载中,同时我也不想 看到一大堆密密麻麻看不懂的东西。 3.客户端希望看到的,是服务器端处理成功或者失
叔牙
2020/11/19
7270
springmvc统一异常拦截方式
《JavaSE-第十九章》之Collection
在没有学习集合前,基本都是用数组存储元素,而数组只适用于元素类型确定以及个数确定,不需要大量的增删的场景。集合却可以完美的解决上述问题,集合在未指定泛型参数时,默认的元素类型为Object,可以存储任意类型的数据,而且无需考虑集合的大小,因为集合的大小是可以动态变化的。所以集合非常适用于做增删元素的场景。
用户10517932
2023/10/07
2030
《JavaSE-第十九章》之Collection
SpringSecurity
springsecurity是安全框架,准确来说是安全管理框架。相比与另外一个安全框架Shiro,springsecurity提供了更丰富的功能,社区资源也比Shiro丰富
捞月亮的小北
2023/12/01
2220
SpringSecurity
4. 上新了Spring,全新一代类型转换机制
上篇文章 介绍完了Spring类型转换早期使用的PropertyEditor详细介绍,关于PropertyEditor现存的资料其实还蛮少的,希望这几篇文章能弥补这块空白,贡献一份微薄之力。
YourBatman
2022/03/08
9390
4. 上新了Spring,全新一代类型转换机制
4. 上新了Spring,全新一代类型转换机制
上篇文章 介绍完了Spring类型转换早期使用的PropertyEditor详细介绍,关于PropertyEditor现存的资料其实还蛮少的,希望这几篇文章能弥补这块空白,贡献一份微薄之力。
YourBatman
2020/12/21
1.3K0
4. 上新了Spring,全新一代类型转换机制
推荐阅读
相关推荐
JavaSE(八)集合之List
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档