通过流式数据平台赋能实时机器学习

原创
02/29 14:17
阅读数 52
龙年启航,共筑辉煌!🐲
 
这是一份实践性指南,教你如何使用Timeplus构建实时欺诈检测系统

机器学习正在迎来实时化的时代

越来越多的机器学习正在自然地向实时化过渡,这一发展主要受到两大关键因素的推动。首先,在异常检测和个性化推荐等应用领域,客户越来越追求即时决策能力。实时预测能够在不断变化的环境中迅速产生响应,从而大幅度提升用户体验。其次,数据基础设施的进步已然可以支持流式数据的处理,这一点得益于Apache Kafka、Redpanda、Apache Pulsar和AWS Kinesis等技术的应用。这些技术为构建实时机器学习所必需的流式数据管道提供了坚实的基础。

构建实时机器学习系统会面临以下的几个挑战:
  • 支持低延迟、实时生成新特征 保持低延迟特性意味着一旦新的数据到达,相关的特征应该在秒级或亚秒级的延迟内进行实时计算。这一点通常需要一个非常成熟的流式处理管道。
  • 保持训练和服务间特征的一致性 在大多数机器学习系统中,用于训练的批处理数据管道与用于推理的流式数据管道不同,这就是典型的Lambda架构。这些管道甚至可能是使用不同的编程语言所创建的,例如,Python用于训练,而Java或Scala用于推理。另一个常见的问题是确保训练数据在某一时间点的正确性,避免使用未来数据进行训练,从而导致数据泄露的情况发生。
  • 回填历史数据 在处理缺失的数据或尝试新特征时,用户通常会使用历史数据回填来训练新模型。这需要整个系统能够回放这些数据以重建所有相关特征。
  • 管理在线和离线数据基础设施的复杂性 为了支持实时机器学习,用户通常需要将用于实时特征生成的流式处理系统添加到现有的基于批处理的数据处理中,才能用于离线特征生成和模型训练,比如Apache Flink。这就使得整个系统变得复杂,而且Flink本身的管理也有些复杂。因此,运行实时机器学习对于拥有强大工程团队的厂商来说是可行的,但并非每个厂商都是阿里或腾讯。

为什么流式数据平台能成为构建实时机器学习的好工具?

为了解决这些挑战,一个典型的解决方案是使用实时特征平台。特征平台帮助用户管理实时特征管道和存储的复杂性,提供批流一体处理,并支持实时机器学习。在这个领域的主要供应商包括Tecton、Hopworks、Claypot、Fennel和Chalk。
另一种选择是利用流式数据平台构建自己的实时特征存储:
  • 这是一个简单的系统,它统一了流式处理和历史数据处理,因此训练和推理能够共享相同的数据处理管道。
  • 它通常提供基于SQL的界面,方便数据科学家和工程师使用。因此,你不需要担心Java/Python之争,你可以在两种语言中调用SQL。
  • 利用流式处理,流式数据平台提供低延迟的数据处理管道,这些管道在数据到达时会以增量的方式逐步处理接收到的数据。
  • 使用流式数据平台回填历史数据十分容易,你只需运行一些SQL便可。
  • 通过使用asof joining来实现“时间点正确性”。

详细地样例参考:使用Timeplus Proton实现用于欺诈检测的实时特征管道

那么,如何构建自己的实时特征管道呢?我创建了这个逐步指南,介绍了我如何使用Timeplus Proton(以及其他开源工具)来实现用于欺诈检测的实时特征管道。 Proton 是我们的开源项目,它是一个在单一内核提供了简单、高效、统一的流式分析与历史分析融合的数据库。
以下是关键组件的高层次架构图:
  • 基于Kaggle上的欺诈检测数据集的在线支付数据生成器。
  • 由Timeplus流式数据平台支持的流式特征管道/存储。
  • 两个实时流,一个用于支付,另一个用于标签。
  • 特征的实时和历史视图。
  • 用于训练和推理数据的所有特征的物化视图。
  • 模型训练,PyCaret,一个用于分类模型训练和推理的低代码机器学习工具。
  • 模型服务,fastapi,一个Python API框架。

在线支付数据

首先,让我们来看看数据。
这里的支付数据是基于Kaggle上的一个欺诈检测数据集。有两个流,一个用于在线支付,另一个用于标记交易是否为欺诈的标签数据。
这些流是使用以下SQL创建的:
CREATE STREAM IF NOT EXISTS online_payments(   `id` string,   `type` enum8('PAYMENT' = 0, 'TRANSFER' = 1, 'CASH_OUT' = 2, 'CASH_IN' = 3, 'DEBIT' = 4),   `amount` float64,   `account_from` string,   `old_balance_from` float64,   `new_balance_from` float64,   `account_to` string,   `old_balance_to` float64,   `new_balance_to` float64)
CREATE STREAM IF NOT EXISTS online_payments_label(    `id` string,    `is_fraud` bool,    `type` string)
这将生成支付数据和标签数据,并将其导入到Proton中。
这里模拟了三种不同类型的欺诈:
  • 类型1: 支付以一个小额转账开始,然后将剩余的金额转账。欺诈者想要验证第一笔付款是否可以成功进行。
  • 类型2: 在非常短的时间内向多个不同账户发送多笔付款,将资金从一个账户发送到多个不同账户。
  • 类型3: 一个账户的所有资金都被用作向商家的付款。
生成器将随机生成这三种不同类型的欺诈交易之一。当交易属于欺诈交易时,会将标签数据保存到标签流中。对于普通交易,不会生成标签数据。

构建特征

"特征"指的是数据的某些单一的、可测量的属性或特性,它们被用作机器学习模型的输入。
在这个示例中,我们将构建三种不同类型的特征,作为我们欺诈检测模型的输入。

实时特征

实时特征是在数据到达时实时生成的。我们使用以下SQL来创建实时特征的视图。
CREATE VIEW IF NOT EXISTS v_fraud_reatime_features ASWITH cte AS  (    SELECT       _tp_time,       id,       type,       account_from,       amount,       lag(amount) AS previous_amount,       lag(_tp_time) AS previous_transaction_time    FROM       default.online_payments    WHERE       _tp_time > earliest_timestamp()    PARTITION BY       account_from  )SELECT   _tp_time,   id,   type,   account_from,   amount,   previous_amount,   previous_transaction_time,   if(previous_transaction_time > earliest_timestamp(), date_diff('second', previous_transaction_time, _tp_time), 0) AS time_to_last_transactionFROM   cte
交易类型和交易金额是两个简单的特征,不需要进行任何处理。这两个特征直接从原始支付数据中创建。
这个SQL创建的另外两个实时特征是:
  • 当前账户的上一笔交易金额
  • 距离当前账户的上一笔交易经过的时间
为了构建这两个特征,使用‘PARTITION BY’子句,它将根据‘account_from’将流分成子流,然后‘lag’函数将返回该账户流中特定字段的先前值。当你想要从流中获取相关值时,这是一个有用的功能,可以用来识别类型1和类型2的欺诈。
一个良好的特征平台应该能够处理有状态的过程。在上述的SQL中,实际上是有状态的处理,因为查询需要记住先前的记录是否与当前的‘account_from’相关。因此,这个特征不仅是根据当前记录生成的,还与历史状态相关。

准实时特征

并非所有特征都是实时特征,准实时特征是在1分钟或5分钟内的时间窗口内构建的特征。这些特征可以用来识别在一段时间内的用户行为。
CREATE VIEW IF NOT EXISTS v_fraud_1m_features ASSELECT   window_start,   account_from,   count(*) AS count,   max(amount) AS max_amount,   min(amount) AS min_amount,   avg(amount) AS avg_amountFROM   tumble(default.online_payments, 60s)WHERE   _tp_time > earliest_timestamp()GROUP BY   window_start, account_from
CREATE VIEW IF NOT EXISTS v_fraud_5m_features ASSELECT   window_start,   account_from,   count_distinct(account_to) AS target_countsFROM   tumble(default.online_payments, 5m)WHERE   _tp_time > earliest_timestamp()GROUP BY   window_start, account_from
通过上述SQL,我们使用滚动窗口构建了一些1分钟和5分钟的特征。
  • 1分钟内每个账户的交易次数
  • 1分钟内每个账户的交易最大金额
  • 1分钟内每个账户的交易最小金额
  • 1分钟内每个账户的平均交易金额
  • 5分钟内每个账户的不同交易目标账户数量
我们可以看到,这些特征对于识别类型2的欺诈可能非常有帮助。

历史特征

历史特征是那些跨越了所有时间的特征。在这个示例中,我们创建了一个1天的滚动窗口聚合,然后使用全局聚合计算了所有聚合结果的最大值/最小值/平均值。
CREATE VIEW IF NOT EXISTS v_fraud_1d_features ASWITH agg1d AS  (    SELECT       window_start, account_from, count(*) AS count, max(amount) AS max_amount    FROM       tumble(default.online_payments, 1d)    WHERE       _tp_time > earliest_timestamp()    GROUP BY       window_start, account_from  )SELECT   now64() as ts, account_from, avg(count) AS avg_count, avg(max_amount) AS avg_max_amountFROM   agg1dGROUP BY   account_from
通过上述SQL,我们创建了以下特征:
  • 每个账户的每日平均交易金额
  • 每个账户的每日平均最大交易金额
这些特征通常对于识别类型3的欺诈非常有用,因为用户一般不会在我们检查他们的消费历史时花掉所有的钱。

使用ASOF join合并所有特征

在训练机器学习模型或进行预测时,需要将特征分配给每个事件。尽管特征是在不同的时间间隔内生成的,但ASOF join从所有特征中选择了每个特征视图中的最新值,如下图所示。
 
CREATE MATERIALIZED VIEW IF NOT EXISTS mv_fraud_all_features ASSELECT   _tp_time AS time,   v_fraud_reatime_features.id AS id,   v_fraud_reatime_features.type AS type,   v_fraud_reatime_features.account_from AS account,   v_fraud_reatime_features.amount AS amount,   v_fraud_reatime_features.previous_amount AS previous_amount,   v_fraud_reatime_features.time_to_last_transaction AS time_to_last_transaction,   v_fraud_1m_features.count AS transaction_count_1m,   v_fraud_1m_features.max_amount AS max_transaction_amount_1m,   v_fraud_1m_features.avg_amount AS avg_transaction_amount_1m,   v_fraud_5m_features.target_counts AS distinct_transaction_target_count_5m,   v_fraud_1d_features.avg_count AS avg_transaction_count_1d,   v_fraud_1d_features.avg_max_amount AS avg_max_transaction_count_1dFROM   v_fraud_reatime_featuresASOF LEFT JOIN v_fraud_1m_features ON (v_fraud_reatime_features.account_from = v_fraud_1m_features.account_from) AND (v_fraud_reatime_features._tp_time >= v_fraud_1m_features.window_start)ASOF LEFT JOIN v_fraud_5m_features ON (v_fraud_reatime_features.account_from = v_fraud_5m_features.account_from) AND (v_fraud_reatime_features._tp_time >= v_fraud_5m_features.window_start)ASOF LEFT JOIN v_fraud_1d_features ON (v_fraud_reatime_features.account_from = v_fraud_1d_features.account_from) AND (v_fraud_reatime_features._tp_time >= v_fraud_1d_features.ts)SETTINGS   keep_versions = 100
我们将上面的查询作为物化视图运行,这是一个长时间运行的后台进程。一旦新事件进入在线支付流中,所有特征将会以实时、增量方式计算。这是保持用于下一步训练和推理的低延迟特征管道的基础。

训练模型

标记和准备训练数据集

在前面的步骤中,我们创建了一个包含所有特征的物化视图,因为我们将训练一个监督分类模型用于识别欺诈,所以我们需要一个带有标签信息的数据集,以指示哪些交易是欺诈的。我们可以简单地将特征物化视图与标签流进行连接。
SELECT *FROM table(mv_fraud_all_features) AS fLEFT JOIN table(online_payments_label) AS l ON f.id = l.id
标签流是一个仅追加日志,其中包含被标记为欺诈的记录,然而,有时我们可能会错误地将一些非欺诈交易标记为欺诈,这时就需要进行更正。如果我们向标签流插入一个新的已更正的标签记录,由于它是一个仅追加日志,现在流中包含两条记录,一条旧的错误标签和一条代表最新的真相的新标签。当我更新标签时,需要使用这个新标签来训练一个新的模型。因此,用户希望使用最新版本的标签,这可以通过使用changelog函数来处理。
SELECT *FROM table(online_payments_label)WHERE id = '37f0e2d5-9786-4833-a0e0-77c13eea4691'
 
Query id: e4139011-89d0-48ad-881b-bac408feda6d┌─id───────────────────────────────────┬─is_fraud─┬─type──┬────────────────_tp_time─┐│ b849c6e3-bc7a-4b20-a8da-35c589565879 │ true     │ type1 │ 2023-11-30 18:31:06.523└──────────────────────────────────────┴──────────┴───────┴─────────────────────────┘┌─id───────────────────────────────────┬─is_fraud─┬─type─┬────────────────_tp_time─┐│ b849c6e3-bc7a-4b20-a8da-35c589565879 │ false    │      │ 2023-12-12 20:01:12.908└──────────────────────────────────────┴──────────┴──────┴─────────────────────────┘
假设我们有一笔交易b849c6e3-bc7a-4b20-a8da-35c589565879,之前标记为欺诈,但之后我们更新为非欺诈。
我们可以使用以下查询来获取所有标签的最新版本:
CREATE VIEW v_latest_labels ASWITH ordered_label AS  (    SELECT       *    FROM       table(online_payments_label)    ORDER BY       _tp_time ASC  )SELECT   id, latest(is_fraud) AS is_fraudFROM   ordered_labelGROUP BY   id
如果运行这个视图,只会返回最新的标签版本。
SELECT   *FROM   v_latest_labelsWHERE   id = 'b849c6e3-bc7a-4b20-a8da-35c589565879'
Query id: 6e6425c9-2bd3-4894-bf54-95d7316b136b┌─id───────────────────────────────────┬─is_fraud─┐│ b849c6e3-bc7a-4b20-a8da-35c589565879 │ false    │└──────────────────────────────────────┴──────────┘
现在我们有了用于训练数据集的查询,其中包含了最新的标签。
SELECT*FROMtable(mv_fraud_all_features) AS fLEFT JOIN v_latest_labels AS l ON f.id = l.id

训练分类模型

现在,我们已经准备好了用于欺诈检测的训练数据集,我们可以构建一个分类模型。PyCaret是一个低代码机器学习工具,可以帮助我们选择要使用的最佳模型,以下是示例代码:
  1. 从Timeplus查询训练数据集
  2. 将查询结果转换为pandas数据框
  3. 初始化PyCaret环境并找到/训练最佳分类模型
  4. 将模型保存到本地文件
import jsonimport pandas as pdfrom pycaret.classification import *from timeplus import Stream, Query, Environment
sql = '''SELECT *FROM table(mv_fraud_all_features) as fLEFT JOIN v_latest_labels as l ON f.id = l.idLIMIT 100000'''
query = (       Query(env=env).sql(query=sql)       .batching_policy(10000, 1000)       .create()   )
query_header = query.header()query_result = []for event in query.result():   if event.event == "message":       query_result += json.loads(event.data)
columns = [ f['name'] for f in query_header]df = pd.DataFrame(query_result, columns=columns)df_train = df[['type', 'amount', 'previous_amount','time_to_last_transaction', 'transaction_count_1m', 'max_transaction_amount_1m','avg_transaction_amount_1m','distinct_transaction_target_count_5m','avg_transaction_count_1d','avg_max_transaction_count_1d','is_fraud']]
s = setup(data = df_train, target = 'is_fraud', session_id = 123)best_model = compare_models()save_model(best_model, 'saved_model')
使用上述代码,我们使用历史查询训练了一个分类模型,其中我们返回了一个有界的数据集。你可以更改时间范围,使用其他条件选择用于训练的特定数据集。

实时推理

利用流式查询,我们可以在事件实时到达时立即运行推理。以下代码展示了如何加载训练模型并进行实时推理:
import jsonimport pandas as pd
from timeplus import Stream, Query, Environmentfrom pycaret.classification import load_model
model = load_model('saved_model')
sql = '''SELECT *FROM mv_fraud_all_featuresWHERE _tp_time > now() -1hLIMIT 3'''
query = (       Query(env=env).sql(query=sql).create()   )
query_header = query.header()columns = [ f['name'] for f in query_header]
for event in query.result():   if event.event == "message":  query_result = []query_result += json.loads(event.data)df = pd.DataFrame(query_result, columns=columns)df_infer = df[['id', 'type', 'amount', 'previous_amount','time_to_last_transaction', 'transaction_count_1m', 'max_transaction_amount_1m','Avg_transaction_amount_1m','distinct_transaction_target_count_5m','avg_transaction_count_1d','avg_max_transaction_count_1d']]  prediction = predict_model(model, data = df_infer)  id = prediction['id'].tolist()[0]    prediction_lable = prediction['prediction_label'].tolist()[0]    is_fraud = 'fraud' if prediction_lable == 1 else 'not fraud'    print(f"transaction {id} is {is_fraud}") 
transaction 23a861db-aabd-424f-943e-d7748ea465a7 is not fraudtransaction 4cc484f4-1668-40c7-a23c-604e871684ab is fraudtransaction e10e3090-3ffe-4a14-9085-7ceef933d8ff is not fraud
现在,你已经训练了一个欺诈检测模型,并可以使用Proton流式处理运行具有实时特征的实时推理!如果你还需要更多功能,比如在SQL中运行预测或使用仪表板实时监控模型性能,你可以使用建立在Proton之上的Timeplus Cloud。企业版提供了更多功能,如SQL探索UI、可视化和仪表板以及警报。那就继续阅读来了解更多关于这些功能的信息吧!

创建一个使用流式SQL运行预测的UDF

为了简化推理过程,我们可以创建一个UDF用于推理,这样实时预测可以在特征管道的同一系统中通过运行流式SQL来运行。
以下代码使用FastAPI来托管欺诈分类模型,然后我们创建了一个名为‘fraud_detect’的UDF。
from typing import Listfrom fastapi import FastAPIfrom pydantic import BaseModel
import pandas as pdfrom pycaret.classification import load_model, predict_model
model = load_model('fraud_model')app = FastAPI()

class PredictItem(BaseModel):   type: List[str]   amount: List[float]   previous_amount: List[float]   time_to_last_transaction: List[int]   transaction_count_1m: List[int]   max_transaction_amount_1m: List[float]   avg_transaction_amount_1m: List[float]   distinct_transaction_target_count_5m: List[int]   avg_transaction_count_1d: List[int]   avg_max_transaction_count_1d: List[int]

@app.get("/")def info():   return {"info": "timeplus fraud detection server"}

@app.post("/predict")def predict(item: PredictItem):   data = []   length = len(item.type)   for i in range(length):       row = [item.type[i], item.amount[i],              item.previous_amount[i], item.time_to_last_transaction[i],              item.transaction_count_1m[i], item.max_transaction_amount_1m[i],              item.avg_transaction_amount_1m[i],              item.distinct_transaction_target_count_5m[i],              item.avg_transaction_count_1d[i],              item.avg_max_transaction_count_1d[i]              ]       data.append(row)
   cols = ['type', 'amount', 'previous_amount',           'time_to_last_transaction', 'transaction_count_1m',           'max_transaction_amount_1m', 'avg_transaction_amount_1m',           'distinct_transaction_target_count_5m', 'avg_transaction_count_1d',           'avg_max_transaction_count_1d']
   df_infer = pd.DataFrame(data, columns=cols)   prediction = predict_model(model, data=df_infer)   prediction_lable = prediction['prediction_label'].tolist()
   return {"result": prediction_lable}
我们可以创建一个 远程UDF ,可以用于使用SQL运行推理。
以下是运行推理的流式SQL。
SELECT id, fraud_detect(to_string(type), amount, previous_amount, time_to_last_transaction, transaction_count_1m, max_transaction_amount_1m, avg_transaction_amount_1m, distinct_transaction_target_count_5m, avg_transaction_count_1d, avg_max_transaction_count_1d) AS predictFROM mv_fraud_all_features

实时监控模型性能

现在,我们已成功部署了我们的特征管道,训练了一个分类模型,使用fastAPI部署了模型,并在Timeplus中创建了一个预测UDF。接下来,监控模型的性能非常重要。由于在这个演示案例中我们有真实标签,我们可以实时显示分类模型的这些指标。
WITH t AS (   SELECT     p._tp_time AS ts, p.id AS id, l.is_fraud AS truth   FROM     online_payments AS p   LEFT JOIN changelog(online_payments_label, id) AS l ON p.id = l.id   settings seek_to = '-1h' ),p AS ( SELECT _tp_time AS ts, id, fraud_detect(to_string(type), amount, previous_amount, time_to_last_transaction, transaction_count_1m, max_transaction_amount_1m, avg_transaction_amount_1m, distinct_transaction_target_count_5m, avg_transaction_count_1d, avg_max_transaction_count_1d) AS predictFROM mv_fraud_all_featuressettings enable_optimize_predicate_expression = 0, seek_to = '-1h')SELECT t.ts AS ts, t.id AS id, t.truth AS truth, (p.predict = 1) AS predictFROM t JOIN p ON t.id = p.id AND date_diff_within(1m, t.ts, p.ts)

上述SQL将返回每个事件的预测值和真实值。使用Timeplus SQL UI,你可以获得真实值和预测值的分布概述。我们将这个查询创建为一个名为‘v_fraud_truth_vs_predict_seekto_1h’的视图,然后可以使用以下SQL来监控模型性能:
WITH metrics AS (   SELECT     ts, truth, predict,     if((truth = true) AND (predict = true), 1, 0) AS TP,     if((truth = true) AND (predict = false), 1, 0) AS FP,     if((truth = false) AND (predict = false), 1, 0) AS TN,     if((truth = false) AND (predict = true), 1, 0) AS FN   FROM     v_fraud_truth_vs_predict_seekto_1h )SELECT window_start, sum(TP + TN) / count() AS accuracy, sum(TP) / (sum(TP) + sum(FP)) AS precision, sum(TP) / (sum(TP) + sum(FN)) AS recallFROM tumble(metrics, ts, 15m)GROUP BY window_start
这个查询返回了过去一小时数据在一个15分钟的滚动窗口内的准确率、精确率和召回率。
另一个查询用于监控预测欺诈与真实欺诈:​​​​​​​
WITH gt AS (SELECT window_start, count(*), 'ground_truth' AS labelFROM tumble(online_payments_label, 1m)where is_fraud = 1GROUP BY window_startSETTINGS seek_to = '-1h'),predict AS ( SELECT window_start, count(*), 'prediction' AS labelFROM tumble(v_detected_fraud, 1m)GROUP BY window_startSETTINGS seek_to = '-1h')SELECT * FROM gtunion SELECT * FROM predict
使用折线图,我们可以清晰地看到趋势。这个实时仪表板可以在我们的 演示仪表板 中找到。

创建检测到的欺诈警报

最后,当检测到欺诈时,采取行动非常重要。Timeplus支持不同的下游数据通道,如Kafka、Slack和Webhook。我们可以将检测到的欺诈发送到任何的通知通道,并在欺诈发生时立即采取行动。

总结

机器学习正在变得实时化。幸运的是,有一些出色的工具可以从开源工具和框架中构建实时机器学习系统。我希望这个教程对你有帮助,这个逐步指南十分容易操作,教你如何利用Proton构建实时特征管道,以满足你对训练/推理机器学习模型的需求。
这里展示的示例代码可以在 Proton的示例文件夹 中找到,我们还有一个 公共演示工作区 。如果你喜欢这个解决方案,请为我们点赞!当然,如果你有任何问题,也请通过后台告诉我们,期待你的反馈!

相关阅读

​​​​​​​欢迎Robert Lau加入Timeplus

我们无比荣幸地宣布,Robert Lau(劉文熙)先生正式加入Timeplus,担任Chief Revenue Officer (CRO)!

作为科技创新业界的领军人物,Robert拥有超过27年的丰富经验。在企业搜索、SIEM、大数据和运营智能等前沿领域积累了深厚的底蕴,尤其在数据分析和实时数据处理方面的卓越成就,帮助众多企业实现高速增长和商业价值的最大化。

长期以来,Robert一直致力于推动科技和商业的深度融合,精于制定并成功实施全球GTM战略。值得一提的是,他深谙中国市场的独特性,强调与本土合作伙伴的紧密合作,对技术创新和业务发展至关重要。

在加入Timeplus之前,他曾在Splunk、Imply、Elastic、ArcSight、Portal Software 等知名公司担任高管,将最新的科技成果转化为商业实践,并引领这些公司成功走向IPO。同时,他还担任多家SaaS和开源初创公司的顾问,为它们提供战略指导和支持。

能够吸引像Robert这样杰出的领袖加入,我们深感自豪。凭借Robert坚实的全球GTM战略实践和多家IPO公司的成功历程,不仅极大地增强了Timeplus团队的实力,更为我们全球业务增长注入了巨大的动力,加速Timeplus攀登新的市场高峰,实现更为宏伟的目标。

再次热烈欢迎Robert加入Timeplus团队!让我们携手共创辉煌!🎉🎉🎉

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部