☰
Google Earth Engine ASSETS批量删除与复制脚本实践
2026/11/6 12:07:03 网站建设 项目流程

删除和复制 Google Earth Engine 里的 ASSETS,看起来是一件很简单的小事,直到你在网页端对着一个存了三百多个中间产物的目录,一个一个点删除点到手抖。我最早接手一个共享项目时就是这么干的,那会儿还觉得“反正就几十个”,结果清理到一半浏览器崩了,页面刷新回来,删到哪个了都不知道,只能从头再数一遍。后来我把这套流程彻底脚本化,JavaScript 和 Python 两套都写过,也包括同事只装了 Python 环境的场景。这篇分享的是真正把“删除指定 ASSETS 里的文件信息”和“复制指定 ASSETS 里的文件信息”做成可重复执行脚本的经验,包含递归列目录、批量删除、跨路径复制、并发控制和幂等处理的完整写法。适合已经会在 Code Editor 里跑几行代码、但一遇到批量操作就回到手动点击的读者,也适合想把清理和迁移工作交给一个定时脚本的人。

1. GEE 的 ASSETS 不是网盘:路径、类型与权限决定了能不能动

很多人第一次接触这个体系,会下意识把它当成云盘:有目录,有文件,能拖能拽,能右键删除。等你真写脚本的时候就会发现,这个直觉会让你连着踩好几个坑。搞清楚它的节点模型,后面的删除和复制才有依据。

1.1 资产 ID 的层级结构与 projects 前缀

每一个资产都由一个全局唯一的asset id定位,形式是projects/项目名/assets/后续路径。这个前缀不是装饰,它是权限边界。你在 Code Editor 左侧看到的目录树,显示的是users/xxx/...或者projects/xxx/assets/...,但真正传给 API 的必须是完整的、带前缀的字符串。自己拼路径时最常见的错误就是把显示名当成了 id,比如把my-folder/sub/img直接传进去,接口会告诉你资产不存在,而实际上它存在,只是少了一段前缀。

路径里的分隔符只有/。别用反斜杠,别带尾部斜杠,也别在路径里塞空格。空格在技术上也许能过,但你在 shell 脚本、日志、CSV 清单之间来回倒手的时候,一个空格就足以让某一行的解析错位。我现在的习惯是:新建资产时就把命名规范定死,只允许小写字母、数字、下划线和短横线,中间产物统一加前缀tmp_,这样后面按前缀筛选删除非常省事。

还有一点容易忽略:folder 和 image collection 不是一回事。folder 是纯容器,里面的东西彼此独立;image collection 是一个整体资产,它下面挂着的 image 属于它的一部分。这个区别直接决定删除策略,第 2 章会展开。

1.2 五类节点:它们的删除语义完全不同

写脚本之前,先得能判断手里这个东西是什么类型。getAsset返回的对象里有type字段,常见的取值有这么几类:

type 取值含义删除时是否需要递归
FOLDER纯目录容器需要,先清空子节点
IMAGE单幅影像不需要,直接删
IMAGE_COLLECTION影像集合不需要,删除时其下 image 一并消失
TABLE矢量表 / FeatureCollection不需要
CLASSIFIER等派生资产训练产物不需要

我第一次写递归删除时,把 image collection 也当目录处理了,挨个去删它下面的 image,结果删到一半报错,因为集合本身已经被前一个操作带走了。后来才明白:只有 FOLDER 需要递归,其它类型都是原子操作。这个认知能让你少写一半代码。

判断类型不要靠猜路径。有人喜欢用“路径最后一段没有扩展名就是目录”,这在你自己建的目录里勉强成立,碰上别人共享过来的资产就完全失效。老老实实调一次getAsset,多花一次网络往返,换来的是不会误删。

1.3 三道权限闸门:不是所有资产你都能删

动手之前必须明确一件事:你只能删自己有权写的东西。具体的限制大致分三层。

第一层是 root。公共目录下的资产是只读的,任何删除和复制写入操作都会被拒绝,这个不用试,试了也是报错。

第二层是项目权限。你在某个项目里有 Writer 或 Owner 角色,才能删这个项目下的资产。共享给你的资产,如果对方只给了 Reader,你同样删不掉。团队协作里经常出现“这个目录看起来在我的列表里,但我删不了”的情况,八成就是权限没给到写。

第三层是资产本身的 ACL。单个资产可以被单独授权,所以理论上可能出现“目录能删、里面某个文件删不掉”的诡异状态。真遇到这种,别急着怀疑脚本,先用网页端对那一个资产试试手删,能立刻区分是代码问题还是权限问题。

提示:任何批量删除脚本在正式跑之前,都应该先跑一次“干跑”(dry run),把将要删除的清单打印出来或者写成文件,人工扫一眼。这一步花两分钟,能救回几个月的成果。

2. 删除:为什么“删目录”这件事必须自己写递归

网页端删除目录时,如果目录非空,通常会提示你先清空。这个限制在 API 层面是一样的:deleteAsset只认单个资产 id,遇到非空 FOLDER 直接报错。所以所谓“删除指定 ASSETS 里的文件信息”,本质上是“列出目录下所有节点 → 自底向上逐个删除 → 最后删空目录”这一串动作。

2.1 deleteAsset 的真实行为与异步陷阱

JavaScript 侧的调用形式是ee.data.deleteAsset(assetId, callback)。这里有个特别容易被忽略的点:它是异步的。如果你不传 callback,在 Code Editor 的交互环境里它表现为同步返回(Code Editor 内部有一套同步 RPC 机制),看起来一切正常;但一旦你把同样的代码搬到 Node 环境或者包进一个批量循环里,行为就可能变成“发起请求后立刻继续下一行”,于是你在一秒内发出几百个删除请求,然后被限流打成一片红色报错。

我早期的写法就是无脑 for 循环,跑二十个资产没事,跑两百个就开始间歇性失败。原因不是权限,也不是路径写错,纯粹是请求发得太密。正确的姿势有两个方向:一是用回调串行化,上一个删完再发下一个;二是控制节奏,每 N 个请求插一个小延迟,并给失败项做退避重试。

Python 侧是ee.data.delete_asset(asset_id),同步阻塞,没有回调概念。表面上看省心,但如果你把它放进大规模循环里,串行执行会非常慢,这时候需要自己上线程池,同时又要压住并发数,避免触发配额。

2.2 手写递归删除的 JavaScript 实现

先把“列全部子节点”这一步封装好。listAssets是分页的,一页默认返回数量有限,目录大了必然要翻页,翻页靠nextPageToken:

// 递归列出一个 folder 下的所有直接子节点(自动翻页) function listChildren(parentId) { var result = []; var token = null; do { var resp = ee.data.listAssets({ parent: parentId, pageSize: 1000, pageToken: token }); result = result.concat(resp.assets || []); token = resp.nextPageToken; } while (token); return result; }

这段在 Code Editor 里可以直接跑,返回的是数组。注意resp.assets里每一项都有id和type,这两个字段就是后面所有逻辑的输入。

有了它,递归删除就是一个很直白的后序遍历:

function deleteNode(assetId, dryRun, log) { var info = ee.data.getAsset(assetId); log.push((dryRun ? '[DRY] ' : '[DEL] ') + assetId + ' (' + info.type + ')'); if (info.type === 'FOLDER') { var children = listChildren(assetId); children.forEach(function (child) { deleteNode(child.id, dryRun, log); // 先把子节点清干净 }); } if (!dryRun) { ee.data.deleteAsset(assetId); // 最后删自己 } } // 用法 var log = []; deleteNode('projects/你的项目名/assets/tmp_2023_batch', true, log); print(log.join('\n'));

后序的含义很明确:先处理子节点,再处理自己。反过来做,删到父目录那一刻就会被“非空”挡下来。

这段代码在跑真实删除时,最大的风险是速度。ee.data.deleteAsset在交互环境里同步返回,一轮几百个调用会拉长执行时间,也容易中途被打断。生产环境里我会把它改成排队形式:先把整棵树的 id 全收集到一个数组,再按批执行,每批之间留间隔,并把成功项写进一个本地清单。

2.3 Python 版递归删除与幂等处理

Python 侧的逻辑一模一样,只是命名风格换了。这里先给一个最朴素的版本:

import ee ee.Initialize(project='你的项目名') def list_children(parent_id): items, token = [], None while True: resp = ee.data.list_assets( parent=parent_id, page_size=1000, page_token=token ) items.extend(resp.get('assets', [])) token = resp.get('nextPageToken') if not token: break return items def delete_node(asset_id, dry_run=True, log=None): log = log if log is not None else [] info = ee.data.get_asset(asset_id) log.append(f"{'[DRY]' if dry_run else '[DEL]'} {asset_id} ({info['type']})") if info['type'] == 'FOLDER': for child in list_children(asset_id): delete_node(child['id'], dry_run, log) if not dry_run: try: ee.data.delete_asset(asset_id) except ee.EEException as e: # 已经不存在就跳过,保证脚本可重复执行 if 'not found' in str(e).lower(): log.append(f'[SKIP] {asset_id}') else: raise return log

关键点在try/except那一段。批量脚本一定会遇到“目标已经不在了”的情况,原因可能是一次中断后重跑,也可能是别人同时在清理。不做幂等处理,整个任务就会卡在一个已经不存在的资产上,后面的全部不执行。判断方式我一般用错误信息里是否包含 not found 之类的关键字,不同客户端版本措辞可能略有差异,实际用的时候把真实异常打印一次看清楚再匹配。

一个真实的踩坑:Python 的分页字段名是nextPageToken,不是next_page_token。我第一次写的时候很自然地按 Python 习惯取了后者,拿到的永远是None,于是脚本只处理了第一页就“圆满完成”了,还打了日志说成功删除 998 个。后来对着账才发现目录里还剩一大半。这个坑非常隐蔽,因为不报错,只是悄悄漏数据。

2.4 删除前的清单快照:这一份表可能比脚本更重要

我现在所有涉及删除的脚本,第一步都是把清单落盘,第二步才是执行。清单里至少包含三列:完整 asset id、类型、父目录。如果是影像集合,再加一列影像数量,方便事后判断删掉的东西量级对不对。

这么做的直接收益有三个。第一,你可以把清单交给同事交叉检查,尤其是删公共目录的时候,多一双眼睛基本能拦住误操作。第二,执行中断后有据可查,知道哪些已经处理过。第三,也是最实际的一点——如果删错了,你能拿着这份表明确知道自己失去了什么,至少能判断要不要重建。

清单格式我一般用 CSV,因为人能直接看,脚本也能直接读。写文件用 Python 侧比较方便,JS 侧在 Code Editor 里可以先print出来再手动存,或者干脆把收集逻辑写成导出任务。

注意:删除操作没有回收站。删掉就是删掉,这一点和本地文件系统完全不同,不要指望“删了再恢复”。

3. 复制:copyAsset 的适用边界比大多数人以为的要窄

删除是减法,复制是搬运。搬运看起来更安全,实际上坑更多,因为复制涉及源、目标、命名和实际数据传输四个环节,每个环节都能出问题。

3.1 copyAsset 能复制什么、不能复制什么

JavaScript 是ee.data.copyAsset(sourceId, destinationId, overwrite, callback),Python 是ee.data.copy_asset(source_id, destination_id, overwrite=False)。这两个函数都只对具体资产生效,也就是 IMAGE、IMAGE_COLLECTION、TABLE 这类。

FOLDER 不能直接复制。这一点是新手最容易卡住的地方:他们对着一个目录调用 copy,期待整个目录搬过去,结果收到一个类型错误。正确的做法仍然是递归:在目标位置先建同构的目录结构,然后逐个资产复制。所以第 2 章的递归框架在这里可以复用,只是把“删除”换成“创建目录 + 复制”。

另外一个限制是跨来源。公共目录里的资产可以作为源被读取和复制到自己名下,但不能作为目标被写入。这个方向性一定要分清,否则会看到“权限不足”却找不到原因。

3.2 同名冲突与 overwrite 参数的版本差异

目标位置已存在同名资产时,行为取决于客户端版本。较新的版本支持显式传overwrite,为真时覆盖,为假时报错。较老的版本没有这个位置参数,函数签名是(sourceId, destinationId, callback)。

我踩过的坑是:本地 Python 环境版本偏旧,代码里写了overwrite=True,结果第三个位置参数被当成回调函数传进去了,报了一个“callback is not callable”之类的错误。看报错完全想不到是版本问题。解决办法很朴素,先打印一下函数签名确认,或者直接用关键字参数传,多数版本会给出更明确的类型错误提示。

在批量场景里,我更倾向于不覆盖。理由很实际:如果目标已存在,有两种可能——我上次已经复制成功了(重复执行),或者目标里有别人放的东西(不能覆盖)。这两种情况都不应该盲目覆盖。所以我一般先getAsset探一下目标是否存在,存在就跳过并记录,不存在才复制。这个判断加上之后,脚本就变成了幂等的,可以随便重跑。

3.3 集合复制后的 id 重写与引用失效

复制一个 image collection,里面的每一幅 image 都会生成新的 id,指向新集合。这意味着,任何写死了旧 image id 的脚本、图表、导出任务都会失效。这一点在迁移工作流时特别致命,因为集合本身复制过去了,看起来一切正常,直到你跑下游流程才发现取不到数据。

我的处理方式是:复制完成后立刻在新集合上跑一次最小验证,比如取第一幅影像的 id 并打印,再读一下集合的影像数量,和源集合比对。两边数量不一致就说明复制没完成或者中途出过错。这个检查写起来只要几行,但能把很多隐性问题在源头拦下来。

src = ee.ImageCollection('projects/旧项目/assets/某个集合') dst = ee.ImageCollection('projects/新项目/assets/某个集合') print('src size:', src.size().getInfo()) print('dst size:', dst.size().getInfo()) print('first id:', dst.first().id().getInfo())

注意size()和getInfo()都是真实计算,大集合上会慢,别在几千幅的集合上随手调。这种校验放在中小规模集合上做足够,大规模集合用资产元数据里的数量字段比对更快。

3.4 大批量复制的配额与限速

复制真实数据是有成本的。一个影像集合可能包含大量影像,每幅都要搬,复制几百个这种集合,时间和配额消耗都很可观。几个实操层面的经验:

并发不能拉满。我试过一次开二十个线程同时复制,前半段很顺,后半段开始大量失败,报的都是限流类的错误。后来把并发压到四到六个,配合失败重试,整体反而更快,因为没有返工。

失败的项要能单独重跑。做法是在清单里加一列状态,每次执行后更新。中断之后只挑状态为失败或未执行的继续,不要从头再来——从头来一遍意味着前面已经成功的项又要被探测一次,时间全浪费在判断“已存在,跳过”上。

大集合分批复制。如果单个集合特别大,可以按时间范围或条带切分,复制成多个小集合,最后再用合并的方式组织。这样每批的失败影响面小,重试成本也低。

4. JavaScript 与 Python 两套写法对照:camelCase 与 snake_case 之间的暗礁

同一个功能有两个语言版本,看起来是便利,实际用起来是两套细节。我见过太多人把 JS 代码里改个下划线就贴进 Python,然后对着莫名其妙的报错排查半天。这部分把关键差异集中列出来。

4.1 函数名对照表

功能JavaScriptPython
列出子资产ee.data.listAssets({parent, pageSize, pageToken})ee.data.list_assets(parent=..., page_size=..., page_token=...)
获取资产元数据ee.data.getAsset(id)ee.data.get_asset(id)
删除资产ee.data.deleteAsset(id, callback)ee.data.delete_asset(id)
复制资产ee.data.copyAsset(src, dst, overwrite, callback)ee.data.copy_asset(src, dst, overwrite=False)
创建目录ee.data.createFolder(path)ee.data.create_folder(path)
重命名/移动ee.data.renameAsset(src, dst)ee.data.rename_asset(src, dst)

表里能看出规律:Python 全部是 snake_case,JS 全部是 camelCase。但参数名不是简单换下划线——JS 里listAssets接收的是一个配置对象,键名是parent、pageSize、pageToken;Python 里是三个独立的关键字参数,名字是parent、page_size、page_token。而返回结果里的分页字段,两边都是nextPageToken,保持原样。这个不对称是最容易写错的地方。

4.2 回调、同步返回与阻塞模型的差异

JS 侧的ee.data函数大多接受一个可选的 callback。在 Code Editor 里不传 callback 时,它走内部的同步机制,表现得像同步函数,可以直接拿返回值。这套机制只存在于交互式编辑环境里,一旦代码离开那个环境,就必须按异步方式处理。

Python 侧没有这个问题,所有调用都是阻塞的,拿到返回值再往下走。代价是慢,好处是逻辑直白。所以我在 Python 里的批量逻辑一般是这样组织的:收集阶段串行(快,都是元数据查询),执行阶段用有限并发(慢,都是真实写入)。

还有一个细节:Python 里要在开头ee.Initialize(),并且指定项目名。不指定项目名在某些环境里能跑,但配额和权限归属会变得不明确,容易在跨项目操作时出问题。JS 侧在 Code Editor 里默认已经初始化好了,切到 Node 环境同样需要显式初始化。

4.3 分页在两边的取法差异

这一点值得单独拎出来说,因为它是“脚本跑成功了但只处理了一部分”这类问题的头号来源。

JS 版:

var token = null; do { var resp = ee.data.listAssets({parent: parentId, pageSize: 1000, pageToken: token}); // 处理 resp.assets token = resp.nextPageToken; } while (token);

Python 版:

token = None while True: resp = ee.data.list_assets(parent=parent_id, page_size=1000, page_token=token) # 处理 resp['assets'] token = resp.get('nextPageToken') if not token: break

注意 Python 里我用了.get(),因为当结果只剩最后一页时,某些版本返回的字典里干脆没有这个键,而不是给一个 None。直接resp['nextPageToken']会抛 KeyError。这种边界情况在实际运行里出现频率不低,尤其是一个目录刚好被 1000 整除的时候。

4.4 环境初始化与凭据的差别

在浏览器编辑环境里基本不需要管凭据,登录状态本身就带着授权。Python 侧在本地跑,第一次要完成一次授权流程,之后会缓存在本地。多项目场景下要显式指定项目,否则默认项目可能不是你想要的,会出现“明明有权限却报没有权限”的怪现象。

我在同事机器上遇到过这种情况:脚本在 A 项目下能正常列出资产,在 B 项目下全部失败。查了半天是默认项目没切过去。定位方法很简单,先打印一下当前初始化用的项目名,一眼就能看出来。

5. 把脚本做到能处理上万个资产的程度

写一个能删十个资产的脚本,跟写一个能删一万个资产、中途断了还能接着跑的脚本,是两件事。这一章讲的是从“能跑”到“敢在正式目录上用”的那段路。

5.1 清单驱动:先枚举,再执行,两阶段分离

最稳的结构是两阶段。第一阶段只做枚举:从一个根目录出发,递归收集所有节点,写成一份带状态列的 CSV。第二阶段只做执行:读取 CSV,按状态筛选,逐条处理,处理完回写状态。

这样做的好处是执行阶段的逻辑变得极简单——它不用再关心目录结构,只面对扁平的列表。而且清单生成一次之后可以反复用,改逻辑、改并发、改重试策略都不需要重新枚举。

清单的列我固定用这几项:asset_id、type、parent、depth、status、error。depth是为删除排序用的,删除时必须由深到浅,深度的降序排列就是天然的执行顺序。

有个细节:递归枚举时,深层次目录很容易混进大量中间产物,比如某个任务的临时切片。我的做法是先按命名规则筛一遍,把带tmp_前缀的单独标出来,执行阶段优先处理这些。真正需要谨慎的是那些名字看起来很正式的资产,它们通常要人工确认。

5.2 并发控制与退避重试

并发数的经验值:元数据查询可以放大一点,八到十问题不大;真实写操作(删除、复制)压到四到六。这个区间是我在不同项目、不同规模下反复试出来的,再高收益就很小,失败率明显上升。

重试策略要用指数退避,不要用固定间隔硬顶。第一次失败等一秒,第二次两秒,第三次四秒,最多试三四次。超过次数就标记为失败写进清单,等人工介入。无脑重试会放大限流,让原本只是慢的任务变成彻底失败。

import time import random def with_retry(fn, max_try=4, base=1.0): for i in range(max_try): try: return fn() except ee.EEException as e: if i == max_try - 1: raise # 加一点随机抖动,避免多个线程同时重试撞在一起 time.sleep(base * (2 ** i) + random.random())

这段逻辑看起来不起眼,但它是让万级任务跑完的关键。我第一次没做重试,跑两千个删除,最后成功了大概八成,剩下两成全是限流报错,还得人工挑出来重跑,反而更麻烦。

5.3 断点续跑与状态回写

状态回写的时机很重要。我一开始是全部跑完再一次性写文件,结果中途断掉,前面几小时的进度全丢了。改成每处理一条就更新内存状态,每二十条落一次盘。这样最坏情况只丢二十条的记录,而那二十条重新探测一下就能知道状态(存在即未完成,不存在即已完成),代价很小。

对于删除,判断“是否已完成”非常简单:目标不存在就是完成了。所以删除任务天然幂等,重跑一次没有任何危害。对于复制,判断要复杂一点:目标存在并不一定意味着复制完整,一个中断的复制可能留下半个集合。这种情况我建议把失败的复制项直接删掉目标再重来,而不是指望它能续传。

5.4 校验:怎么确认复制真的成功了

复制完成后的校验步骤绝对不能省。我的做法是分两层。

第一层是元数据层:比对源和目标的类型、影像数量(或要素数量)。数量对不上就标记异常。这一层很快,适合全量做。

第二层是数据层:随机抽一到两幅影像,各取一个小范围的统计值,比如均值或者是波段数,和源对比。这一层慢,只对关键资产做。抽样检查能抓到一类很隐蔽的问题——数量对得上但内容不对,比如源和目标其实指错了地方。

还有一种情况值得留个心眼:复制过程中源被修改了。这在共享项目里不是不可能,复制出来的结果和源对不上。遇到数量或统计值不一致又找不到别的原因,可以回头确认一下源集合的更新时间。

5.5 一个容易被忽略的旁路:命令行工具

在浏览器里写 JavaScript 和在本地写 Python 之外,还有一条路——官方的命令行工具,支持列出、复制、移动、删除这类常见操作,其中列目录还带递归选项。它的好处是不用写循环,一行命令就能看完整棵树;缺点是细粒度控制不如 API,比如你要按类型筛选、按前缀跳过、做退避重试,还是得自己写。

我的实际用法是混合的:日常小规模清理直接用命令行,几秒钟搞定;需要可追溯、可重复、带校验的批量任务才上脚本。命令行那一步通常用来快速核对目录规模和结构,相当于给脚本的枚举结果做个交叉验证。

6. 只有真跑过才会知道的几个细节

这一章收集的是一些零散但确实让人卡住过的点,写出来省得别人再走一遍。

名字里的特殊字符。路径里带空格、括号、中文的资产,在脚本里原样传递一般是没问题的,但一旦经过 shell 或者 CSV,就会出问题。CSV 里的逗号如果不加引号,整行会错位,导致删错对象。我现在所有清单都用csv模块或 pandas 写,不手拼字符串,就是为了避开这个。

误删之后能补救到哪一步。结论很干脆:删掉的资产本身无法恢复。但有两件事可以做。一是如果你之前在别的路径复制过一份,那还能拿回来;二是如果删除的是一个 image collection 而里面的影像曾经被单独引用过,那些引用也一并失效了。所以我在清理前,凡是不确定以后还会不会用到的东西,先复制到一个归档目录再删。归档目录的命名固定,方便定期清理。

共享资产的只读属性。别人共享给你的资产,即使出现在你的列表里,你也不能删、不能覆盖复制。这类资产在执行阶段会被拒绝,脚本要把这种失败和限流失败区分开——前者重试再多次也没用,应该直接标记为“跳过”并从后续重试列表里剔除,否则你的重试机制会一直在一个永远不可能成功的项上空转,白白拖慢整体进度。

根目录级别的操作要单独对待。项目 assets 根节点本身是删不掉的,脚本如果从根开始递归,最后一步会必然失败。处理办法是在删除逻辑里加一层判断,遇到根节点直接跳过。这个 bug 通常在你把脚本从子目录切到根目录跑的时候才暴露出来。

复制跨项目时的写入目标。目标必须是一个你确实有写权限的项目路径。我遇到过一次失败排查很久,最后发现目标路径里的项目名拼错了一个字母,接口自作聪明地新建了一个同名目录结构,然后把数据写了进去,直到下一次跑才因为找不到资产才发现。所以复制之后的那次get_asset探测,不只是校验,也是防止这类“看起来成功”的假象。

我个人在实际操作中的体会是,这套东西的技术难度并不高,难的是让脚本在出问题时能停下来、能说清楚停在哪、能安全地接着跑。真正花时间的地方不是那几十行 API 调用,而是清单、状态、重试、校验这些看起来像“附加功能”的部分。如果你只打算清理一次、规模也就几十个资产,手点和脚本区别不大;但只要开始出现“这个目录里有上千个中间产物,每季度要清一次”的场景,把上面这套骨架搭起来,后面每一次都是几分钟的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询