中国移动爱购商城混合下单流程拆解与Python实现思路

📅 2026/7/1 18:08:27
中国移动爱购商城混合下单流程拆解与Python实现思路
中国移动爱购商城「混合下单」流程拆解与 Python 实现思路卡券自动抢购 / 库迪 / 星巴克 / 奈雪 / 和平精英关键词爱购商城、卡券自动下单、混合下单、星巴克券、库迪券、奈雪券、和平精英、移动积分、和包、Python 爬虫、HTTP 自动化一、前言最近在研究中国移动爱购商城 / 权益市场域名*.coc.10086.cn上的卡券自动抢购方案涉及的商品包括但不限于库迪咖啡券星巴克券奈雪的茶券和平精英道具 / 礼包以及一些常见的五折购卡券这套系统下单流程里最复杂的一块就是所谓的「混合下单mixedOrder」——它不像普通商品那样走简单的submitOrder而是要在真正下单之前完成一次完整的风控握手。下面把整条链路按照用浏览器抓包的角度拆开讲一遍给有同样需求的同学一个整体实现思路。⚠️ 本文只讲流程 伪代码 整体架构不暴露任何具体加密算法、公钥、硬编码环境指纹。⚠️ 仅供学习研究使用请勿用于任何商业 / 灰产用途。二、整体下单链路一张图看懂把整个流程画成时间轴从登录到下单成功一共 9 步登录 (smsLogin / appLogin) ↓ 详情页预热 (getProductDetailById × 2) ↓ 积分计算 (bestPriceCalculate) ↓ 积分抵扣查询 (hasDeductionSku) ↓ 优惠券选择 (selectCoupon) ↓ 权益账户查询 (projectAccount/info) ↓ 风控黑名单校验 (checkBlacklist) ↓ 风控握手 (wasmparams ← gettoken) ↓ 短信验证码 mixedOrder 下单可以分成 3 个阶段预热阶段模拟用户进详情页风控阶段算 digitalBean、扣减额度、选券下单阶段验证码 加密风控参数 真正下单三、阶段一登录爱购商城有两种登录方式登录方式入口备注App 登录LoginDevCocByMethodAsync复用移动 App 已有的 token短信登录LoginDevCocWithSmsAsync抓短信 RSA 加密登录伪代码deflogin(phone:str,method:str)-str: 返回 token-DevCoc (写进 cookie, 后面所有请求都要带) ifmethodsms:# 1) 请求下发短信验证码send_sms(phone,encrypt_phoneencrypt_rsa(phone))# 2) 等用户输入验证码 (或从短信猫/通知拿到)sms_codewait_for_sms(phone,timeout60)# 3) 用 RSA 加密手机号 验证码, 登录resppost(/coc3/gr/login/ssoRights/userLoginV2,{m1:encrypt_rsa(phone),m2:md5(reverse_base64(phone)SALTphone),s1:encrypt_rsa(sms_code),s2:md5(sms_codeSALTreverse_base64(sms_code)),ticket:MOBILE,ticketType:7,businessType:RIGHTS_MARKET,})returnparse_token_from_cookie(resp)else:# appreturnuse_existing_app_token(phone)登录成功之后两个 cookie 必须保留token-DevCoc所有 dev.coc 接口的身份凭证User-Token部分接口额外需要四、阶段二详情页预热进详情页之前浏览器会先发两次完全相同的getProductDetailById请求这是为了模拟用户从列表页点进来的行为。服务端不会因此校验更多东西跳过也大概率能下但偶尔会被风控挡。defwarmup_product_page(sku_id:int)-str: 返回 pageUrl (后续 wasmparams 里要原样塞回, 不能乱改) # 拼一个标准 detail 页面 URL, 这个 URL 必须和浏览器一致page_urlbuild_detail_url(midsku_id,paytype7,# 五折购固定 7rule_codeconfig.rule_code,# 配置里的活动规则码channel_codeconfig.channel_code,member_id153,# 浏览器默认 memberIdpage_recordedtrue,)# 浏览器实际会请求两次, 第一次是为了初始化, 第二次是真正拿数据for_inrange(2):get(url/coc3/coc3-market/arrange/getProductDetailById,params{id:sku_id,isNeedDesc:false,t:now_unix()},headers{phone:token_dev_coc,referer:page_url,user-agent:固定 UA,...})returnpage_url坑点pageUrl 后面 wasmparams 校验里要原样塞回去任何 query 顺序 / 编码方式不一致都会被风控打回火爆。建议直接用浏览器抓包抓的 URL 当作模板。五、阶段三积分 抵扣 选券这 4 个接口是一组预计算请求目的是告诉服务端我准备用多少 digitalBean 哪张券。defprepare_order(sku_id:int,page_url:str)-PreparedOrder:# 1) 算最划算的 digitalBean 用量 实际应付best_pricepost(/coc3/coc3-market/arrange/bestPriceCalculate,params{skuId:sku_id,operationType:0,isApp:1,t:now_unix()},headers{phone:token_dev_coc,referer:page_url,...})digital_beanbest_price[canUseSzd]orbest_price[canMaxAib]or0pay_amountbest_price[discountsPrice]orbest_price[price]# 2) 查这个 SKU 能不能积分抵扣has_deductpost(/coc3/coc3-market/api/integralAccount/hasDeductionSku,body{skuId:sku_id,projectAccountId:2},headers{content-type:application/json,referer:page_url,...})# 3) 选券 (没券就发个空请求, 让服务端知道我选过了)post(/coc3/coc3-market-card/api/card/selectCoupon,headers{phone:token_dev_coc,skuid:str(sku_id),# 注意是 skuid 不是 skuIdcontent-length:0,referer:page_url,...})# 4) 查权益账户余额post(/coc3/gr-integral-ability/api/external/score/projectAccount/info,body{accountType:QUAN_YI_JIN,businessType:2},headers{token:token_dev_coc,...})# 5) 风控黑名单post(/coc3/coc3-market-order/api/external/black/checkBlacklist,body{skuId:sku_id},headers{phone:token_dev_coc,...})returnPreparedOrder(digital_beandigital_bean,pay_amountpay_amount,page_urlpage_url,)4 个请求之间的先后顺序是固定的服务端会按调用顺序锁账号状态不能并发。六、阶段四风控握手wasmparams最难的一步这是整条链路里最恶心的部分。爱购商城在浏览器里跑了一个 WASM 模块前端采集指纹 加密 验签一体它会做两件事在浏览器侧采集一堆前端环境指纹50 个字段覆盖 canvas、webgl、cookie、错误栈等用这些指纹 一个verification签名 AES-256-GCM 加密 RSA 加密 AES key最终产出wasmparams [cocAurora, cipherText]服务端拿到这两个值之后会用同一套规则反算 verification、再用 RSA 解开 AES key、再用 AES-GCM 解开密文最后比对指纹。实现思路伪代码defbuild_wasmparams(phone_token:str,page_url:str)-list: 复刻浏览器 WASM 模块的两步加密流程 返回 [RSA-Base64, AES-GCM-Base64] # Step 1: 构造前端环境指纹 env (50 字段) # 这些字段在浏览器里是实时采集的, 服务端会校验关键几个:# - page_url (必须和上面 warmup 时拼的一致)# - user_agent# - time_elapsed (从进入页面到现在的毫秒数, 30~60s 之间最稳)# - 浏览器渲染相关指纹 (canvas / webgl / cookie / 错误栈 等)# 其它字段 (mime_types / plugins / hardware_concurrency ...) 大多可以固定envbuild_browser_env(page_url,time_elapsedrandint(30000,60000))# Step 2: 计算 verification 签名 # 算法核心 (用伪代码描述):# - 拼接一个 canonical 字符串 (按 env 字段的固定顺序)# - 再选其中一个字段算 selected digest# - 最终 sha256(canonical ts selected_digest 服务端固定盐值)# 关键坑:# - 字段顺序敏感, env.keys() 的插入顺序就是 canonical 拼接顺序# - 模数是 min(10, len(env)), 不是 len(env)# - time_elapsed 走整数无引号# - device_pixel_ratio 走 float 必须带小数点# - 其它值直接 json.dumps, dict / 嵌套对象必须直接 dumptsint(time.time()*1000)verificationcompute_verification(env,ts)# Step 3: gettoken 那一步 # 把 { data, environment, timestamp, verification } 整个 JSON 加密:# AES-256-GCM - 输出 nonce(12) || ct || tag(16)# RSA-PKCS1v15 加密 AES key - cocAurorapayload{data:{...},environment:env,timestamp:ts,verification:verification}coc_aurora_1,cipher_text_1rsa_aesgcm_encrypt(payload,pub_keyRSA_PUBKEY)# 发请求拿 deviceTokendevice_tokenpost(/coc3/gr/risk-ability/wasm/gettoken,body{benefit_phone:phone_token,encryptedText:cipher_text_1,timestamp:ts,},headers{coc-aurora:coc_aurora_1,referer:page_url,...})# Step 4: mixedOrder 加密 # env 这次只有 5 个字段: deviceToken / platform / time_elapsed / url / user_agentenv2{deviceToken:device_token,platform:Linux aarch64,time_elapsed:time_elapsed139,# 注意 139url:page_url,user_agent:UA,}payload2{data:{},environment:env2,timestamp:ts139,verification:compute_verification(env2,ts139)}coc_aurora_2,cipher_text_2rsa_aesgcm_encrypt(payload2,pub_keyRSA_PUBKEY)return[coc_aurora_2,cipher_text_2]wasmparams最终就是json.dumps([cocAurora, cipherText])直接当字符串塞 HTTP header 的wasmparams字段。6.1 RSA AES-GCM 加密defrsa_aesgcm_encrypt(plaintext:dict,pub_key_pem:str)-tuple: 复刻 C# RSA.Encrypt(..., RSAEncryptionPadding.Pkcs1) AesGcm aes_keyos.urandom(32)nonceos.urandom(12)# RSA 加密 AES keyrsaRSA.import_key(pub_key_pem)coc_aurorabase64.b64encode(PKCS1_v1_5.new(rsa).encrypt(aes_key)).decode()# AES-GCM 加密cipherAES.new(aes_key,AES.MODE_GCM,noncenonce)ct,tagcipher.encrypt_and_digest(json.dumps(plaintext,ensure_asciiFalse,separators(,,:)).encode())# 输出: nonce(12) || ct(N) || tag(16), 整体 base64cipher_textbase64.b64encode(noncecttag).decode()returncoc_aurora,cipher_text6.2 verification 计算defcompute_verification(env:dict,ts:int)-str:keyslist(env.keys())# 顺序敏感!ts_textstr(ts)modulusmin(10,len(keys))# 模数 min(10, 字段数)idxts%modulusifidx0:idxmodulus selected_keykeys[idx]selected_valueenv[selected_key]# canonical: 拼 key json(key, value)canonical.join(f{k}{_serialize(k,env[k])}forkinkeys)# selected digestsel_strf{selected_key}{_serialize(selected_key,selected_value)}{ts_text}selected_digesthashlib.sha256(sel_str.encode(utf-8)).digest()# final# 注: 末尾要拼一个服务端写死的魔数字符串 (盐值), 整个 verification 才算完整# 真实盐值需要从浏览器 WASM 模块里反推, 这里不写出来mergedcanonical.encode(utf-8)ts_text.encode(utf-8)selected_digestbSALTreturnhashlib.sha256(merged).hexdigest()def_serialize(key:str,value)-str:ifkeytime_elapsed:# 整数无引号returnstr(int(value))ifkeydevice_pixel_ratio:# 浮点必须带小数点srepr(float(value))returnsif.insoreinselses.0returnjson.dumps(value,ensure_asciiFalse)# 其它 (含 dict) 直接 dump上面只展示了算法骨架和关键约束顺序、模数、特殊字段、加密套路真正能从浏览器里抠出来的指纹字段值、错误栈字符串、verification 校验用到的魔数盐值、RSA 公钥这些都没在本文放出来——大家自己拿浏览器 DevTools 抓 WASM 模块的导出函数反推就行。七、阶段五验证码 下单风控参数拿到后剩下的就是正常下单流程defplace_order(sku_id:int,sms_code:str,wasmparams:list,digital_bean:int):resppost(/coc3/coc3-market-order/api/external/mixedOrder,body{receivers:phone_token,skuId:encrypt_sku_id(sku_id),# skuId 也要 RSA 加密member:True,smsCode:sms_code,digitalBean:digital_bean,couponBatchId:...,# 可选...},headers{wasmparams:json.dumps(wasmparams),# ← 第六步算出来的phone:phone_token,...})returnresp返回code0就说明下单成功下一个短信猫收验证码 → 循环跑下一个手机号。八、整体调度伪代码defrun_one_phone(phone:str,config:OrderConfig):# 1) 登录tokenlogin(phone,methodconfig.login_method)phone_tokentoken# 2) 详情页预热page_urlwarmup_product_page(config.sku_id)# 3) 预计算 digitalBean / 选券prepprepare_order(config.sku_id,page_url)# 4) 风控握手wasmparamsbuild_wasmparams(phone_token,page_url)# 5) 请求发短信request_sms_code(config.sku_id,phone_token,prep.digital_bean)# 6) 等短信sms_codewait_for_sms(phone,timeout60)# 7) 真正下单resultplace_order(config.sku_id,sms_code,wasmparams,prep.digital_bean)returnresult九、常见坑 排查思路现象大概率原因排查方向返回火爆/riskpageUrl 拼错 / wasmparams 加密不对抓浏览器真包对比 query / env / verification返回code ! 0但没具体原因token 过期 / 风控黑名单重新登录连续 3 次换 IPgettoken 返回 200 但没 deviceTokenRSA 公钥不匹配 / JSON 序列化转义不一致公钥从浏览器 wheel 包里再抠一次确认ensure_asciiFalse, separators(,, :)verification 一直对不上字段顺序 / 特殊字段序列化方式在浏览器抓到的真 JSON 上逐字段对比下单时返回smsCode invalid短信超时 / 验证码没及时拿到短信猫和并发数要匹配十、扩展哪些商品能下库迪咖啡各种满减券、5 折券星巴克中杯升杯券、买一赠一券奈雪的茶满减券、饮品券和平精英皮肤、点券、礼包各种五折购活动paytype7各种视频会员腾讯/爱奇艺/优酷 月卡只要 skuId 配置对得上都是同一套流程。只是不同活动的ruleCode/channelCode/tc不一样pageUrl 要跟着改。十一、写在最后本文不提供RSA 公钥、不提供 verification 用的盐值、不提供浏览器侧的固定指纹字符串 —— 真正在生产环境里跑必须自己拿浏览器真包反推因为服务端升级时这些细节会变。真要批量跑请控制并发 控制好 cookie 池 控制好短信猫节奏别把账号搞炸了。这套东西有 50% 的难度在风控剩下 50% 在登录态维护和验证码延迟。如果你在实现过程中遇到具体问题比如某个 verification 怎么算的、或者抓包怎么定位字段可以评论区交流。也可以地球联系 chaego1企鹅 MTQ1NDA0MjExNw觉得有帮助的可以点赞 收藏。