尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

k6性能测试实战:动态参数处理与Token关联技术详解

k6性能测试实战:动态参数处理与Token关联技术详解
📅 发布时间:2026/7/27 8:08:54

1. 项目概述:为什么动态参数是性能测试的“灵魂”?

做性能测试,尤其是API压测,最怕遇到什么?脚本跑起来,看着并发数上去了,结果一看报告,全是401、403或者业务逻辑错误。很多时候,问题就出在脚本太“死板”了——它只会用第一次请求拿到的固定值去跑后续成千上万的请求。在真实的用户场景里,每个虚拟用户(VU)都应该有自己的会话状态,其中最核心的就是登录后获取的Token,以及依赖这个Token或其他接口返回数据的动态参数。

这就是今天要聊的核心:用k6处理动态参数,特别是登录Token的获取和接口参数之间的关联。k6作为一个现代化的、开发者友好的性能测试工具,用JavaScript(ES6+)写脚本,天生就适合处理这类动态逻辑。它不像一些传统工具那样依赖录制回放或复杂的XML配置,而是让你用写代码的方式,清晰地定义用户行为流和数据流转。当你需要压测一个需要先登录、然后带着Token去操作、并且后续请求参数依赖前序接口响应的系统时,这套方法就是你的“瑞士军刀”。

简单说,这解决了性能测试脚本的“真实性”和“可复用性”两大痛点。一个能正确处理动态参数的脚本,才能模拟出真实用户的并发行为,压测结果也才有参考价值。无论是Grafana k6的官方教程,还是社区里大家讨论的从JMeter脚本转化过来,动态参数处理都是必过的坎。接下来,我会拆解整个流程,从设计思路到每一行代码怎么写,再到实际踩过的坑,让你能直接上手,写出健壮、高效的k6压测脚本。

2. 核心设计思路:从“静态播放”到“动态会话”

在动手写代码之前,我们先要把思路理清楚。一个处理动态参数的k6脚本,其核心设计理念是模拟一个有状态的用户会话,而不是执行一串孤立的HTTP请求。

2.1 理解“动态参数”的两种主要类型

根据我的经验,在API测试中,动态参数主要分为两类,处理方式也略有不同:

  1. 会话型动态参数:以登录Token为代表。它的特点是:

    • 获取时机:通常在脚本最开始,通过一次认证请求(登录接口)获得。
    • 使用范围:在整个虚拟用户(VU)的生命周期内有效,需要被携带在后续几乎所有请求的头部(如Authorization: Bearer <token>)。
    • 生命周期:与用户会话绑定,可能有过期时间,但在单次迭代中我们通常认为它不变。
    • 核心挑战:如何在一个VU内安全地存储和传递这个值,避免不同VU之间的Token串用。
  2. 数据关联型动态参数:这类参数的值来源于前一个接口的响应体。

    • 典型场景:创建订单后返回的orderId,需要用来查询订单详情或进行支付;发布文章后返回的articleId,用于后续的编辑、删除操作。
    • 特点:它的值是在脚本运行过程中动态产生的,每个VU、甚至同一VU的每次迭代都可能不同。
    • 核心挑战:如何从复杂的JSON或HTML响应中,精准、可靠地提取出目标值,并传递给下一个请求。

2.2 k6的脚本生命周期与变量作用域

k6脚本的执行遵循特定的生命周期,理解这一点对设计数据流至关重要:

  • init代码:在压测开始前,所有VU生成之前,执行且仅执行一次。适合加载静态测试数据(如从CSV文件读取用户名列表)、配置全局环境变量。
  • default函数:这是每个VU的入口点,也是压测阶段反复执行的部分。我们主要的业务逻辑(登录、操作、注销)都写在这里。default函数每次执行称为一次迭代。
  • VU作用域:在default函数内声明的变量(用let或const),其作用域仅限于当次迭代。这意味着,如果你在迭代开始时登录获取了Token,它自然可以在本次迭代的后续请求中使用。但下一次迭代开始时,这些变量会被重置。
  • 闭包与模块:我们可以利用JavaScript的闭包特性,或者在init阶段定义一些返回函数的模块,来创建一些在VU迭代间保持状态或提供工具方法的上下文,这常用于管理复杂的会话状态。

基于以上理解,我们的设计思路就很明确了:在default函数的单次迭代内,串联起一个完整的用户操作流。首先调用登录接口,从响应中提取Token并存入变量;然后,在发起后续请求时,将这个Token变量设置到请求头中;如果后续请求需要前序接口的返回ID,则同样在收到响应后立即解析并保存,用于构造下一个请求的URL或Body。

3. 实战演练:登录Token获取与传递

理论说再多,不如一行代码。我们从一个最简单的登录场景开始,逐步增加复杂度。

3.1 基础版:硬编码用户登录

假设我们有一个登录接口POST /api/v1/auth/login,它接收JSON格式的账号密码,成功则返回一个包含Token的JSON响应。

import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { vus: 10, duration: '30s', }; export default function () { // 1. 定义登录请求的载荷 const loginPayload = JSON.stringify({ username: 'testuser@example.com', password: 'yourPassword123', }); const loginParams = { headers: { 'Content-Type': 'application/json' }, }; // 2. 发送登录请求 const loginRes = http.post('https://your-api.com/api/v1/auth/login', loginPayload, loginParams); // 3. 检查登录是否成功,并提取Token check(loginRes, { '登录成功': (r) => r.status === 200, }); const authToken = loginRes.json('token'); // 假设响应体是 { "token": "eyJhbGciOiJ..." } // 4. 使用Token访问一个受保护的接口 const protectedParams = { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json', }, }; const profileRes = http.get('https://your-api.com/api/v1/user/profile', protectedParams); check(profileRes, { '获取资料成功': (r) => r.status === 200, }); sleep(1); }

代码解读与注意事项:

  • loginRes.json(‘token’):这是k6提供的便捷方法,用于从JSON响应体中通过JSONPath提取值。这里的’token’是一个简单的路径。如果响应结构是{“data”: {“accessToken”: “xxx”}},则应写为loginRes.json(‘data.accessToken’)。
  • 检查点(Check):check()函数至关重要。它不仅仅是为了让报告好看,更是脚本健壮性的保障。如果登录失败(返回401等),loginRes.json(‘token’)可能会因为响应体不是JSON或路径不对而抛出异常,导致脚本中止。因此,先检查状态码是200,再提取数据,是一个好习惯。
  • Token的放置:通常Token放在Authorization请求头中,格式多为Bearer <token>。你需要根据被测系统的具体约定进行调整,可能是Token <token>或者直接放在一个自定义头里,如X-Access-Token。

注意:上面的例子使用了硬编码的账号密码。在真实压测中,这会导致所有VU都用同一个账号登录,不仅无法模拟多用户场景,还可能触发系统的账号风控策略(如频繁登录限制)。接下来我们就要解决这个问题。

3.2 进阶版:使用外部文件实现多用户登录

为了让每个VU使用不同的身份,我们需要一个用户凭证池。k6可以方便地使用外部JSON或CSV文件。

首先,创建一个users.json文件:

[ {"username": "user1@test.com", "password": "pass1"}, {"username": "user2@test.com", "password": "pass2"}, // ... 更多用户 ]

然后,修改脚本:

import http from 'k6/http'; import { check, sleep } from 'k6'; import { SharedArray } from 'k6/data'; // 在init阶段加载用户数据,SharedArray会以更节省内存的方式在所有VU间共享数据 const users = new SharedArray('users', function () { return JSON.parse(open('./users.json')); }); export const options = { vus: 10, duration: '30s', }; export default function () { // 为当前VU的本次迭代,随机选择一个用户(也可以按顺序取) const user = users[Math.floor(Math.random() * users.length)]; // 或者按VU编号和迭代数取模,确保分布均匀:const user = users[__VU % users.length]; const loginPayload = JSON.stringify({ username: user.username, password: user.password, }); const loginParams = { headers: { 'Content-Type': 'application/json' }, }; const loginRes = http.post('https://your-api.com/api/v1/auth/login', loginPayload, loginParams); // 更健壮的检查:确保状态码为200且响应体包含token字段 const loginCheck = check(loginRes, { '登录成功': (r) => r.status === 200, '响应包含token': (r) => { try { const body = r.json(); return body.token !== undefined; } catch (e) { return false; // 如果响应不是JSON,也视为失败 } }, }); // 如果登录失败,本次迭代可以提前结束,避免使用无效Token if (!loginCheck) { console.log(`VU ${__VU} 迭代 ${__ITER} 登录失败,状态码: ${loginRes.status}`); return; // 直接返回,跳过后续请求 } const authToken = loginRes.json('token'); // 后续携带Token的请求... const protectedParams = { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json', }, }; // ... 发起业务请求 sleep(1); }

实操心得:

  • SharedArrayvsopen():在init代码中,我们使用SharedArray来加载数据。与直接使用open()相比,SharedArray的内容在VU之间是只读共享的,能极大减少内存消耗,特别适合大型数据文件。open()函数则更适合在init阶段加载一些配置类的小文件。
  • 用户选择策略:Math.random()简单但可能导致负载不均衡。对于严格的测试,使用__VU(虚拟用户ID)和__ITER(迭代次数)进行计算,可以确保每个VU每次迭代使用不同的、可预测的用户数据,便于问题复现。
  • 错误处理:增加了对登录响应的更细致检查,并在失败时使用return提前结束当前迭代。这能防止脚本因无效Token而报出一连串无意义的错误,让测试报告更清晰。console.log配合__VU和__ITER内置变量,能帮你快速定位是哪个虚拟用户的哪次迭代出了问题。

4. 复杂场景:接口参数关联与链式调用

登录只是第一步,真正的业务压测往往涉及一系列有依赖关系的接口。比如:用户登录 -> 浏览商品列表 -> 选择第一个商品加入购物车 -> 查看购物车 -> 创建订单 -> 支付。

4.1 从响应中提取动态ID

我们以“加入购物车”和“查看购物车”为例。假设加入购物车接口返回新创建的购物车项目ID (cartItemId)。

export default function () { // ... 之前的登录代码,获取 authToken ... // 1. 获取商品列表(假设第一个接口) const productsRes = http.get('https://your-api.com/api/v1/products', { headers: { 'Authorization': `Bearer ${authToken}` }, }); check(productsRes, { '获取商品成功': (r) => r.status === 200 }); const products = productsRes.json(); const firstProductId = products[0].id; // 假设列表第一个商品的ID // 2. 将商品加入购物车 const addToCartPayload = JSON.stringify({ productId: firstProductId, quantity: 1, }); const addToCartRes = http.post('https://your-api.com/api/v1/cart/items', addToCartPayload, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json', }, }); check(addToCartRes, { '加入购物车成功': (r) => r.status === 201, // 创建成功常返回201 }); // 3. 关键步骤:从响应中提取动态的 cartItemId const cartItemId = addToCartRes.json('id'); // 假设响应是 { "id": "cart_item_abc123", ... } // 4. 使用提取的 cartItemId 查询或操作特定购物车项目 // 例如:更新该商品数量 const updatePayload = JSON.stringify({ quantity: 2 }); const updateRes = http.put(`https://your-api.com/api/v1/cart/items/${cartItemId}`, updatePayload, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json', }, }); check(updateRes, { '更新购物车成功': (r) => r.status === 200 }); // 或者:删除该商品 // const deleteRes = http.del(`https://your-api.com/api/v1/cart/items/${cartItemId}`, null, { // headers: { 'Authorization': `Bearer ${authToken}` }, // }); sleep(1); }

4.2 处理复杂的响应结构

现实中的API响应可能嵌套很深,或者包含数组。k6的.json()方法支持简单的JSONPath表达式。

// 假设登录响应为:{ "data": { "user": { "accessToken": "xyz", "refreshToken": "abc" } } } const authToken = loginRes.json('data.user.accessToken'); const refreshToken = loginRes.json('data.user.refreshToken'); // 假设商品列表响应为:{ "items": [ { "id": 1, "name": "商品A" }, { "id": 2, "name": "商品B" } ] } const products = productsRes.json('items'); const secondProductId = products[1].id; // 获取第二个商品的ID // 如果响应是一个数组,直接通过索引获取 // 假设响应是:[ { "id": 100 }, { "id": 101 } ] const firstItemId = productsRes.json()[0].id;

常见问题与排查技巧:

  • undefined错误:如果你尝试访问一个不存在的JSON路径,k6会返回undefined。在后续使用这个值(比如拼接到URL中)时,可能会导致脚本错误。一个好的实践是在使用前进行判断。
    const cartItemId = addToCartRes.json('id'); if (!cartItemId) { console.error(`无法从响应中提取cartItemId。响应状态: ${addToCartRes.status}, 响应体: ${addToCartRes.body}`); return; // 提前结束迭代 }
  • 响应格式多变:有些API成功和失败的响应格式完全不同。成功时返回{“data”: {...}},失败时返回{“error”: “...”}。直接用固定路径提取可能会失败。更稳妥的做法是先判断状态码,或者使用try-catch。
    let orderId; try { const body = createOrderRes.json(); if (createOrderRes.status === 200 || createOrderRes.status === 201) { orderId = body.data.orderId; // 成功路径 } else { orderId = body.error?.referenceId; // 失败时可能有其他ID } } catch (e) { console.log(`解析响应JSON失败: ${e}, 原始body: ${createOrderRes.body}`); }

5. 架构优化:使用闭包管理会话状态

当业务流非常长,或者你想让代码更清晰、更易于复用时,可以将整个用户会话封装成一个对象或函数。这里介绍一种利用JavaScript闭包创建“用户会话上下文”的模式。

// 我们可以创建一个“会话管理器”,它持有Token,并提供一系列方法 function createUserSession(username, password) { // 私有变量,存储该会话的Token let authToken = null; let refreshToken = null; // 登录方法 const login = () => { const res = http.post('https://your-api.com/api/v1/auth/login', JSON.stringify({ username, password }), { headers: { 'Content-Type': 'application/json' } } ); if (res.status === 200) { const body = res.json(); authToken = body.accessToken; refreshToken = body.refreshToken; console.log(`用户 ${username} 登录成功`); return true; } else { console.log(`用户 ${username} 登录失败: ${res.status}`); return false; } }; // 创建一个预置了认证头的请求参数 const getAuthHeaders = (additionalHeaders = {}) => { if (!authToken) { throw new Error('请先登录!'); } return { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json', ...additionalHeaders, // 可以合并额外的请求头 }, }; }; // 业务方法:获取用户资料 const getProfile = () => { return http.get('https://your-api.com/api/v1/user/profile', getAuthHeaders()); }; // 业务方法:创建订单 (假设需要商品ID) const createOrder = (productId, quantity) => { const payload = JSON.stringify({ productId, quantity }); return http.post('https://your-api.com/api/v1/orders', payload, getAuthHeaders()); }; // 返回一个包含公共方法的对象 return { login, getProfile, createOrder, // 也可以暴露一个获取当前token的方法(谨慎) getToken: () => authToken, }; } // 在init阶段加载用户数据 const userCredentials = new SharedArray('creds', function() { return JSON.parse(open('./users.json')); }); export default function () { const cred = userCredentials[__VU % userCredentials.length]; const session = createUserSession(cred.username, cred.password); // 使用会话 if (!session.login()) { return; // 登录失败,结束迭代 } // 获取资料 const profileRes = session.getProfile(); check(profileRes, { '资料获取成功': (r) => r.status === 200 }); // 创建订单(这里简化,假设我们知道一个商品ID) const orderRes = session.createOrder('prod_123', 1); if (orderRes.status === 201) { const orderId = orderRes.json('id'); console.log(`订单创建成功,ID: ${orderId}`); // 接下来可以用这个orderId去做其他操作... } sleep(1); }

这种架构的优势:

  1. 高内聚:所有与某个用户会话相关的状态(Token)和方法(API调用)都封装在一起,逻辑清晰。
  2. 易复用:createUserSession像一个工厂函数,可以轻松地为每个VU创建独立的会话实例,互不干扰。
  3. 易维护:如果认证方式改变(比如从Bearer Token改成API Key),你只需要修改getAuthHeaders这一个地方。
  4. 可扩展:可以轻松地在createUserSession内部加入Token刷新逻辑、请求重试机制等。

6. 高级技巧与避坑指南

在实际编写和运行k6动态参数脚本时,我总结了一些容易踩坑的地方和对应的解决方案。

6.1 Token过期与刷新逻辑

压测持续时间较长时,Token可能会过期。一个健壮的脚本应该能处理这种情况。

function createUserSession(username, password) { let authToken = null; let refreshToken = null; let tokenExpiry = null; // 可以记录过期时间 const login = () => { /* ... 同上,登录后设置token和过期时间 ... */ }; const refreshAuthToken = () => { const res = http.post('https://your-api.com/api/v1/auth/refresh', JSON.stringify({ refreshToken }), { headers: { 'Content-Type': 'application/json' } } ); if (res.status === 200) { const body = res.json(); authToken = body.accessToken; // 可能也会返回新的refreshToken refreshToken = body.refreshToken || refreshToken; tokenExpiry = Date.now() + (body.expires_in * 1000); // 计算新的过期时间 return true; } return false; }; const getAuthHeaders = (additionalHeaders = {}) => { // 简单版本:每次请求前检查Token是否存在,不存在则登录(适合迭代开始时登录) if (!authToken) { if (!login()) { throw new Error('登录失败,无法获取认证头'); } } // 复杂版本:检查Token是否即将过期,是则刷新 // if (tokenExpiry && Date.now() > tokenExpiry - 60000) { // 提前1分钟刷新 // if (!refreshAuthToken()) { // // 刷新失败,尝试重新登录 // if (!login()) { // throw new Error('Token刷新和重新登录均失败'); // } // } // } return { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json', ...additionalHeaders, }, }; }; // ... 其他业务方法 }

注意:实现完整的Token自动刷新逻辑会显著增加脚本复杂度,并可能影响压测本身对服务器造成的压力模型(因为增加了刷新请求)。对于短时间(如几分钟)的压测,通常可以忽略刷新,让每个迭代重新登录即可。对于长时间稳定性压测,则需要根据实际情况设计。

6.2 处理非JSON响应(如HTML、XML)

不是所有接口都返回JSON。例如,一些老旧系统或SSO登录页面可能返回HTML。

import { parseHTML } from 'k6/html'; // 需要导入html模块 export default function () { // 假设第一个请求获取一个包含CSRF Token的登录页面 const getPageRes = http.get('https://old-system.com/login'); const doc = parseHTML(getPageRes.body); // 将HTML响应解析为文档对象 const csrfToken = doc.find('input[name="csrf_token"]').first().attr('value'); // 使用选择器提取值 // 使用提取的CSRF Token构造登录请求 const loginPayload = `username=test&password=pass&csrf_token=${csrfToken}`; const loginRes = http.post('https://old-system.com/login', loginPayload, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); // ... 后续可能从响应Header(如Set-Cookie)或Body中提取会话信息 }

6.3 参数化与数据驱动测试

对于创建订单、发布内容等操作,我们往往需要不同的请求参数。可以将测试数据放在外部文件中。

test_data.csv:

productId,quantity,expectedPrice prod_001,1,99.99 prod_002,2,199.98 prod_003,1,149.50

脚本:

import { SharedArray } from 'k6/data'; import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js'; const testData = new SharedArray('test data', function () { // 使用papaparse解析CSV const data = papaparse.parse(open('./test_data.csv'), { header: true }).data; return data; }); export default function () { // ... 登录 ... // 为本次迭代选择一组测试数据 const dataRow = testData[__ITER % testData.length]; const orderPayload = JSON.stringify({ productId: dataRow.productId, quantity: parseInt(dataRow.quantity), }); const orderRes = http.post('https://your-api.com/api/v1/orders', orderPayload, getAuthHeaders()); check(orderRes, { '订单创建成功': (r) => r.status === 201, // 甚至可以校验响应中的价格是否与预期一致 '金额校验正确': (r) => r.status === 201 && r.json('totalAmount') === parseFloat(dataRow.expectedPrice), }); }

6.4 常见问题速查表

问题现象可能原因排查步骤与解决方案
脚本运行大量失败,状态码401Token未正确传递或已过期。1. 使用console.log打印出请求前的Authorization头,确认Token值正确且格式无误。
2. 检查登录请求是否真的成功(状态码200,且有Token返回)。
3. 确认Token在后续请求的Header中名称正确(是Authorization还是X-Auth-Token)。
4. 对于长时压测,考虑实现Token刷新逻辑或每个迭代重新登录。
.json()提取返回undefined或报错JSON路径错误,或响应根本不是JSON。1. 先console.log(res.body)打印原始响应,确认其结构和内容。
2. 使用try-catch包裹res.json()调用。
3. 对于非JSON响应,使用parseHTML或正则表达式提取数据。
4. 使用check先验证状态码和内容类型。
不同VU间数据串扰错误地使用了全局变量或在init中定义了可变状态。1. 确保每个VU的会话状态(如Token)定义在default函数内部或通过闭包隔离。
2. 使用SharedArray加载的只读数据是安全的,但不要修改它。
3. 避免在init中定义会被default函数修改的变量。
性能测试结果不真实,QPS很低脚本中存在不必要的串行等待或阻塞操作。1. 检查sleep()的使用是否合理,是否可以移除或缩短。
2. 确认动态参数提取和请求构造的逻辑是否高效,避免复杂的同步操作(如读写大型文件)。
3. 使用batch()进行并行请求,如果业务允许。
从JMeter脚本转化过来后逻辑错误JMeter的上下文变量(如正则表达式提取器)与k6的JavaScript变量作用域不同。1. 仔细梳理JMeter脚本中变量的传递路径(通常通过${varName})。
2. 在k6中,用JavaScript变量明确模拟这一传递过程,将提取的值赋给一个变量,然后在需要的地方使用该变量。

最后,我个人最深刻的体会是:日志是你的好朋友。在脚本的关键节点,比如登录成功后、提取重要参数后、发起关键请求前,使用console.log输出相关变量(可以结合__VU和__ITER),能让你在控制台或输出日志中清晰地看到每个虚拟用户的执行轨迹。当测试出现意外失败时,这些日志是定位问题最快、最直接的手段。不要怕输出太多,k6允许你通过--console-output参数将日志导入文件,方便事后分析。

相关新闻

  • AI辅助修复Blender CATS插件并开发Unity导出工具实战
  • 免费视频转文字工具推荐:先排除三类不合适的再入选 - 软件小管家
  • DM6467硬件设计核心:仿真控制、电源时钟与上拉电阻实战解析

最新新闻

  • 炉石传说HsMod终极指南:5分钟解锁32倍速和200+皮肤定制
  • Claude Code安装和使用教程—接入deepseek模型和GLM等其他三方模型
  • AI降重工具评测与学术论文优化技巧
  • KVM主题:GPU直通与显卡虚拟化基础解析
  • USB2.0 24位高精度多路测温与多功能测控一体化采集卡
  • 斯特林数C++实现:从数学原理到高精度计算与动态规划优化

日新闻

  • OpenClaw开源智能体网关:AI助手与即时通讯的完美融合
  • 写一个简单的sh脚本
  • 2026年 西安缝隙天线厂家:5G通信与车载天线专业定制供应商深度分析 - 卓企推荐

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号