├── .eslintignore ├── tbk-api-server ├── taobao │ ├── test.js │ ├── lib │ │ ├── tmc │ │ │ ├── common.js │ │ │ ├── tmcClient.js │ │ │ └── tmcCodec.js │ │ ├── topUtil.js │ │ ├── spiUtil.js │ │ └── api │ │ │ ├── dingtalkClient.js │ │ │ ├── topClient.js │ │ │ └── network.js │ ├── index.js │ └── main.js ├── jd │ ├── .gitignore │ ├── src │ │ ├── constants.js │ │ ├── request.js │ │ ├── sign.js │ │ └── client.js │ ├── main.js │ ├── README.md │ └── index.js ├── imgs │ └── tbk-api-deno1.png ├── README.md ├── index.js └── pdd │ └── index.js ├── .gitignore ├── wechat ├── README.md ├── common │ ├── reply.js │ └── index.js ├── db_action │ ├── test.js │ └── index.js ├── msg.js ├── handlers │ ├── on-friend.js │ └── on-message.js ├── service │ ├── msg-filter-service.js │ └── msg-filters.js └── index.js ├── .eslintrc.js ├── schedule └── index.js ├── License ├── Dockerfile ├── package.json ├── config └── index.js ├── README.md └── utils └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | tbk-api-server/jd/src/* 2 | tbk-api-server/taobao/lib/* -------------------------------------------------------------------------------- /tbk-api-server/taobao/test.js: -------------------------------------------------------------------------------- 1 | const { updateOrders } = require('./index'); 2 | 3 | updateOrders(); 4 | -------------------------------------------------------------------------------- /tbk-api-server/jd/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | yarn.lock 4 | yarn-error.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /tbk-api-server/imgs/tbk-api-deno1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pea-cake/tbk/HEAD/tbk-api-server/imgs/tbk-api-deno1.png -------------------------------------------------------------------------------- /tbk-api-server/jd/src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DEFAULT_SERVER: 'https://api.jd.com/routerjson' 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */.settings 2 | */target 3 | *.project 4 | *.classpath 5 | *.idea 6 | *.vscode 7 | *.iml 8 | *.DS_Store 9 | /build/ 10 | /bin/ 11 | *node_modules 12 | */log 13 | .env 14 | *.memory-card.json 15 | package-lock.json -------------------------------------------------------------------------------- /wechat/README.md: -------------------------------------------------------------------------------- 1 | # 淘宝客微信bot服务 2 | 3 | * 首先完成([转链服务源码](../tbk-api-server/README.md)) 4 | * 安装mongodb并创建数据库名为'taobaoke' 5 | * 执行: 6 | ``` 7 | npm i ( 网不好可能需要一些时间,下载大文件: puppeteer) 8 | npm run wechat 9 | ``` 10 | 11 | * 手机微信扫码登录即可 12 | -------------------------------------------------------------------------------- /tbk-api-server/jd/main.js: -------------------------------------------------------------------------------- 1 | const sign = require('./src/sign'); 2 | const request = require('./src/request'); 3 | const Client = require('./src/client'); 4 | 5 | const create = (obj = {}) => new Client(obj); 6 | 7 | module.exports = { 8 | sign, 9 | request, 10 | create, 11 | }; 12 | -------------------------------------------------------------------------------- /wechat/common/reply.js: -------------------------------------------------------------------------------- 1 | const service = require('../service/msg-filter-service'); 2 | /** 3 | * 获取私聊返回内容 4 | */ 5 | async function getContactTextReply(that, contact, msg) { 6 | const result = await service.filterFriendMsg(that, contact, msg); 7 | console.log('result', result); 8 | return result; 9 | } 10 | 11 | module.exports = { 12 | getContactTextReply, 13 | }; 14 | -------------------------------------------------------------------------------- /wechat/db_action/test.js: -------------------------------------------------------------------------------- 1 | const { ObjectId } = require('mongodb'); 2 | const db_action = require('./index'); 3 | 4 | async function main() { 5 | const db_obj = await db_action.connectMongo('taobaoke'); 6 | const customers = await db_action.selectCustomers(db_obj, { _id: ObjectId('61bf8e63d7ea1e8adfff90fe') }); 7 | const customer = customers[0]; 8 | console.log(customer); 9 | } 10 | main(); 11 | -------------------------------------------------------------------------------- /tbk-api-server/README.md: -------------------------------------------------------------------------------- 1 | # 淘宝、京东、拼多多转链服务 2 | * ../config/index.js 文件 配置你的联盟平台返利key secret 3 | * 启动服务: 4 | ``` 5 | npm i 6 | npm run server 7 | ``` 8 | * 使用: 9 | * 请求地址:http://127.0.0.1:3333?url='淘宝、京东、拼多多复制的商品链接(只要包含链接内容即可,无需提取出https://****.com链接)' 如:'【淘宝】https://m.tb.cn/h.U0ptZX4?tk=eCtH2FfjT0a CZ0001 「Lilbetter仿麂皮夹克男秋装新款男装美式上衣休闲工装秋季外套潮」 10 | 点击链接直接打开' 11 | * 示例: 12 | -------------------------------------------------------------------------------- /wechat/msg.js: -------------------------------------------------------------------------------- 1 | const newTip = '我是购物小助手:现已支持 京东、淘宝、拼多多平台查询商品优惠券,下单后还可拿补贴!'; 2 | const useMsg = ` 3 | 【食用步骤】如下: 4 | 1.复制淘宝商品/京东/拼多多商品链接发给我 5 | 2.即可看到补贴详细信息,领取优惠券并下单 6 | 3.付款后复制订单号给我绑定补贴 7 | 4.补贴将在收货完成5-15天内,通过微信🧧方式发送给您 8 | 9 | `; 10 | // const menuBtn = '更多功能' 11 | 12 | const menuMsg = ``; 13 | 14 | const commonMsg = `输入的内容有误,你是想查商品优惠券和补贴信息吗?${useMsg}`; 15 | 16 | const subscribeMsg = `亲,终于等到了您!${menuMsg}${newTip}${useMsg}`; 17 | 18 | module.exports = { 19 | subscribeMsg, 20 | menuMsg, 21 | commonMsg, 22 | useMsg, 23 | }; 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true, 6 | }, 7 | extends: "airbnb-base", 8 | overrides: [ 9 | ], 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | }, 13 | rules: { 14 | camelcase: 0, 15 | "no-underscore-dangle": 0, 16 | "import/no-unresolved": 0, 17 | indent: ['error', 4], 18 | quotes: "off", 19 | "no-console": "off", 20 | "no-unused-vars": "off", 21 | "no-unreachable": "off", 22 | "no-redeclare": "warn", 23 | eqeqeq: 0, 24 | "no-await-in-loop": 0, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/tmc/common.js: -------------------------------------------------------------------------------- 1 | var Common = function(){ 2 | } 3 | 4 | Common.enum = { 5 | MessageType:{ 6 | CONNECT: 0, 7 | CONNECTACK: 1, 8 | SEND: 2, 9 | SENDACK:3 10 | }, 11 | HeaderType : { 12 | EndOfHeaders : 0, 13 | Custom: 1, 14 | StatusCode : 2, 15 | StatusPhrase: 3, 16 | Flag : 4, 17 | Token : 5 18 | }, 19 | 20 | ValueFormat : { 21 | Void : 0, 22 | CountedString : 1, 23 | Byte : 2, 24 | Int16 : 3, 25 | Int32 : 4, 26 | Int64 : 5, 27 | Date : 6, 28 | ByteArray : 7 29 | }, 30 | MessageKind :{ 31 | None: 0, 32 | PullRequest : 1, 33 | Confirm : 2, 34 | Data : 3 35 | } 36 | } 37 | 38 | exports.Common = Common; -------------------------------------------------------------------------------- /tbk-api-server/jd/src/request.js: -------------------------------------------------------------------------------- 1 | const req = require('request-promise'); 2 | const format = require('dateformat'); 3 | const sign = require('./sign'); 4 | const {DEFAULT_SERVER} = require('./constants'); 5 | 6 | module.exports = async (url = DEFAULT_SERVER, method, param_json, version = '1.0', access_token = '', app_key = '', app_secret = '') => { 7 | const isNewVersion = url === DEFAULT_SERVER; 8 | const qs = { 9 | method, 10 | v: version, 11 | access_token, 12 | app_key, 13 | timestamp: format(new Date(), 'yyyy-mm-dd HH:MM:ss'), 14 | sign_method: 'md5', 15 | format: 'json' 16 | }; 17 | const params = typeof param_json === 'object' ? JSON.stringify(param_json) : param_json; 18 | if (isNewVersion) { 19 | qs['360buy_param_json'] = params; 20 | } else { 21 | qs['param_json'] = params; 22 | } 23 | qs.sign = sign(qs, app_key, app_secret, url); 24 | return await req({ 25 | uri: url, 26 | qs, 27 | json: true 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /schedule/index.js: -------------------------------------------------------------------------------- 1 | const schedule = require('node-schedule'); 2 | // date 参数 3 | 4 | // 其他规则见 https://www.npmjs.com/package/node-schedule 5 | // 规则参数讲解, 规则类似 Linux 下的 crontab ('*'代表通配符) 6 | // 7 | // * * * * * * 8 | // ┬ ┬ ┬ ┬ ┬ ┬ 9 | // │ │ │ │ │ | 10 | // │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) 11 | // │ │ │ │ └───── month (1 - 12) 12 | // │ │ │ └────────── day of month (1 - 31) 13 | // │ │ └─────────────── hour (0 - 23) 14 | // │ └──────────────────── minute (0 - 59) 15 | // └───────────────────────── second (0 - 59, OPTIONAL) 16 | 17 | // 1. 按固定时间触发 18 | // 19 | // 每分钟的第30秒触发: '30 * * * * *' 20 | // 21 | // 每小时的1分30秒触发 :'30 1 * * * *' 22 | // 23 | // 每天的凌晨1点1分30秒触发 :'30 1 1 * * *' 24 | // 25 | // 每月的1日1点1分30秒触发 :'30 1 1 1 * *' 26 | // 27 | // 每周1的1点1分30秒触发 :'30 1 1 * * 1' 28 | 29 | // 2. 按时间差触发 (日/月/周同理) 30 | // 31 | // 每30分钟触发一次: '0 */30 * * * *' 32 | // 33 | // 每6小时触发一次: '0 0 */6 * * *' 34 | 35 | function setSchedule(date, callback) { 36 | schedule.scheduleJob({ tz: 'Asia/Shanghai', rule: date }, callback); 37 | } 38 | 39 | module.exports = { 40 | setSchedule, 41 | }; 42 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 pea-cake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tbk-api-server/jd/src/sign.js: -------------------------------------------------------------------------------- 1 | const md5 = require('md5'); 2 | const {DEFAULT_SERVER} = require('./constants'); 3 | 4 | module.exports = (obj = {}, app_key = '', app_secret = '', url = DEFAULT_SERVER) => { 5 | const isNewVersion = url === DEFAULT_SERVER; 6 | const list = []; 7 | if (typeof obj['app_key'] === 'undefined') { 8 | obj['app_key'] = app_key; 9 | } 10 | const params = obj['360buy_param_json'] || obj['param_json']; 11 | if (isNewVersion) { 12 | delete obj['param_json']; 13 | obj['360buy_param_json'] = params; 14 | } else { 15 | delete obj['360buy_param_json']; 16 | obj['param_json'] = params; 17 | } 18 | Object.keys(obj).sort().forEach(key => { 19 | const value = obj[key]; 20 | if (key !== 'access_token' || key!='format' || (typeof value === 'string' && value.length > 0)) { 21 | list.push(`${key}${typeof value === 'object' ? JSON.stringify(value) : value}`); 22 | } 23 | }); 24 | // console.log('list--->',list) 25 | const signStr = `${app_secret}${list.join('')}${app_secret}`; 26 | // console.log('signStr--->',signStr) 27 | return md5(signStr).toUpperCase(); 28 | }; 29 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/index.js: -------------------------------------------------------------------------------- 1 | const getLink = require('./main'); 2 | const config = require('../../config'); 3 | 4 | const getPromoteLink = function (tb_url) { 5 | return getLink(tb_url).then((res) => { 6 | const resultData = { 7 | goods_name: (res && res.short_title) || '', 8 | platform: '淘宝', 9 | }; 10 | if (res) { 11 | if (res.coupon_id) { 12 | resultData.coupon = `${res.coupon_amount}元 (${res.coupon_info})`; 13 | } 14 | resultData.url = `${res.tkl_info.split(' ')[0]}(复制淘口令到淘宝)`; 15 | if (res.commission_rate > 0) { 16 | // eslint-disable-next-line max-len 17 | let money = (Number(res.commission_rate) / (100 * 100)) * (parseFloat(res.zk_final_price) - parseFloat(res.coupon_amount || 0)); 18 | money = (Number(money) * config.RAKE).toFixed(2); 19 | if ((!Number.isNaN(money)) && money) { 20 | resultData.money = money; 21 | } 22 | } 23 | return resultData; 24 | } 25 | return null; 26 | }); 27 | }; 28 | module.exports = { 29 | getPromoteLink, 30 | }; 31 | -------------------------------------------------------------------------------- /tbk-api-server/jd/src/client.js: -------------------------------------------------------------------------------- 1 | const sign = require('./sign'); 2 | const request = require('./request'); 3 | const {DEFAULT_SERVER} = require('./constants'); 4 | 5 | module.exports = class Client { 6 | constructor(obj = {}) { 7 | this.setConfig(obj); 8 | } 9 | 10 | setConfig(obj = {}) { 11 | const {appKey, appSecret, serverUrl} = obj; 12 | if (typeof appKey !== 'string' || appKey.length < 1) { 13 | throw new Error('请正确填写app_key,类型为字符串'); 14 | } 15 | if (typeof appSecret !== 'string' || appSecret.length < 1) { 16 | throw new Error('请正确填写appSecret,类型为字符串'); 17 | } 18 | this.appKey = appKey; 19 | this.appSecret = appSecret; 20 | this.serverUrl = serverUrl || DEFAULT_SERVER; 21 | } 22 | 23 | async request(method, param_json, version = '1.0', access_token = '') { 24 | const {appKey, appSecret, serverUrl} = this; 25 | return await request(serverUrl, method, param_json, version, access_token, appKey, appSecret); 26 | } 27 | 28 | sign(obj) { 29 | const {appKey, appSecret, serverUrl} = this; 30 | return sign(obj, appKey, appSecret, serverUrl); 31 | } 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.13.1 2 | LABEL author="peacake" 3 | ENV PORT=3333 4 | WORKDIR / 5 | COPY . /tbk 6 | RUN apt-get update 7 | RUN apt-get install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget libgbm1 -y 8 | RUN wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.4.18.tgz && tar -zxvf mongodb-linux-x86_64-debian10-4.4.18.tgz && rm -rf mongodb-linux-x86_64-debian10-4.4.18.tgz && mv mongodb-linux-x86_64-debian10-4.4.18 /usr/local/mongodb4 && export PATH=/usr/local/mongodb4/bin:$PATH 9 | RUN mkdir -p /var/lib/mongo && mkdir -p /var/log/mongodb && chown `whoami` /var/lib/mongo && chown `whoami` /var/log/mongodb 10 | RUN cd /usr/local/mongodb4/bin && ./mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork 11 | RUN npm install -g cnpm --registry=https://registry.npm.taobao.org 12 | RUN cd /tbk && cnpm install 13 | RUN cnpm install -g pm2 14 | EXPOSE ${PORT} 15 | CMD cd /tbk && pm2 start tbk-api-server/index.js -n tbk-api-server && pm2 start wechat/index.js -n wechat && pm2 log 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tbk", 3 | "version": "1.0.0", 4 | "description": "淘宝客(淘宝、京东、拼多多购物返利系统)", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "server": "cd ./tbk-api-server && node index.js", 9 | "wechat": "cd ./wechat && node index.js", 10 | "lint": "eslint . --ext .js --fix" 11 | }, 12 | "keywords": [ 13 | "淘宝客" 14 | ], 15 | "author": "pea-cake", 16 | "license": "MIT", 17 | "dependencies": { 18 | "axios": "^0.24.0", 19 | "cheerio": "^1.0.0-rc.2", 20 | "dateformat": "^3.0.3", 21 | "file-box": "^1.4.15", 22 | "form-data": "^0.2.0", 23 | "iconv-lite": "^0.4.7", 24 | "install": "^0.13.0", 25 | "md5": "^2.2.1", 26 | "mime": "~1.3.4", 27 | "moment": "^2.29.1", 28 | "mongodb": "^4.2.2", 29 | "node-machine-id": "^1.1.12", 30 | "node-schedule": "^1.3.2", 31 | "pdd-sdk": "^1.0.13", 32 | "qrcode-terminal": "^0.12.0", 33 | "qs": "^6.10.2", 34 | "request": "^2.88.2", 35 | "request-promise": "^4.2.5", 36 | "urlencode": "^1.1.0", 37 | "ws": "^0.4.32", 38 | "wechaty": "^1.20.2", 39 | "wechaty-puppet": "^1.18.3", 40 | "wechaty-puppet-wechat": "^1.18.4" 41 | }, 42 | "devDependencies": { 43 | "eslint": "^8.23.0", 44 | "eslint-config-airbnb-base": "^15.0.0", 45 | "eslint-plugin-import": "^2.26.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const msgs = require('../wechat/msg'); 2 | // 配置文件 3 | module.exports = { 4 | RAKE: 0.92, // 返利给用户的比例 5 | JDconfig: { 6 | // 京东联盟 7 | appKey: "", 8 | appSecret: "", 9 | }, 10 | TBconfig: { 11 | // 阿里联盟 12 | appkey: "", 13 | appsecret: "", 14 | adzone_id: "", // 推广位pid 'mm_123_456_789' 的789就是adzone_id 获取链接 https://pub.alimama.com/third/manage/record/adzone.htm 15 | }, 16 | PDDconfig: { 17 | // 多多客 18 | clientId: "", 19 | clientSecret: "", 20 | pid: "", // 推广位pid 21 | }, 22 | autoAcceptFriend: true, // 自动接受好友 23 | acceptFriendKeyWords: [], // 接受好友认证关键词 24 | newFriendReplys: [{ type: 1, content: msgs.subscribeMsg, url: '' }], // 新好友回复 25 | replyKeywords: [ 26 | { 27 | reg: 1, 28 | keywords: ['查', '优惠', '券', '京东', '淘宝', '拼多多'], 29 | replys: [{ type: 1, content: msgs.useMsg, url: '' }], 30 | }, 31 | { 32 | reg: 1, 33 | keywords: ['你好', '打招呼', '我是', '已添加', 'hello', '哈喽'], 34 | replys: [{ type: 1, content: msgs.subscribeMsg, url: '' }], 35 | }, 36 | { 37 | reg: 1, 38 | keywords: ['你是', '你叫啥'], 39 | replys: [{ type: 1, content: `我是购物您的小助手,${msgs.useMsg}`, url: '' }], 40 | }, 41 | ], 42 | heartService: { // 检测登录状态,暂时先别用了,容易封号提醒 43 | SENDDATE: '00 00 * * * *', // 定时发送时间 每小时发送一次,规则见 /schedule/index.js 44 | vx: '', // 微信心跳发送人,微信号或昵称 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /tbk-api-server/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const qs = require('qs'); 3 | const parseUrl = require('url'); 4 | const taobaoSDK = require('./taobao'); 5 | const jdSDK = require('./jd'); 6 | const pddSDK = require('./pdd'); 7 | const { getHttpString, formatReplayStr } = require('../utils'); 8 | 9 | // 创建淘宝客api服务 10 | http.createServer(async (request, response) => { 11 | // 解析请求,包括文件名 12 | const router = parseUrl.parse(request.url); 13 | console.log('请求地址:', router.pathname); 14 | const { query } = router; 15 | const params = qs.parse(query); 16 | console.log('接收到的url参数', params); 17 | let responseData = {}; 18 | const goal_url = getHttpString(params.url); 19 | if (goal_url.indexOf('tb.cn') != -1 || goal_url.indexOf('taobao.com') != -1) { 20 | // 未处理的url参数直接传入,主要就是'「'和'」'之间的关键词,如'「绿联八类网线万兆cat8七类7六6类千兆超家用宽带电脑路由器网络线」' 21 | const res = await taobaoSDK.getPromoteLink(params.url); 22 | if (res) { 23 | responseData = res; 24 | } 25 | } else if (goal_url.indexOf('jd.com') != -1) { 26 | responseData = await jdSDK.getPromotionReply(goal_url); 27 | } else if (goal_url.indexOf('yangkeduo.com') != -1 || goal_url.indexOf('pinduoduo.com') != -1) { 28 | responseData = await pddSDK.getPromoteLink(goal_url); 29 | } else { 30 | responseData = {}; 31 | } 32 | // 输出请求的文件名 33 | response.writeHead(200, { 'Content-Type': 'text/plain;charset=utf-8' }); 34 | // 发送响应数据 35 | // 响应文件内容 36 | responseData = formatReplayStr(responseData); 37 | response.write(responseData); 38 | response.end(); 39 | }).listen(3333); 40 | 41 | // 控制台会输出以下信息 42 | console.log('Server running at http://127.0.0.1:3333/'); 43 | -------------------------------------------------------------------------------- /wechat/handlers/on-friend.js: -------------------------------------------------------------------------------- 1 | const { Friendship } = require("wechaty"); 2 | const { delay } = require("../../utils"); 3 | const config = require("../../config"); 4 | 5 | /** 6 | * 好友添加 7 | */ 8 | async function onFriend(friendship) { 9 | console.log("-------->friendship:", friendship); 10 | try { 11 | let logMsg; 12 | const name = friendship.contact().name(); 13 | const hello = friendship.hello(); 14 | logMsg = `${name},发送了好友请求`; 15 | console.log(logMsg); 16 | if (config.autoAcceptFriend) { 17 | switch (friendship.type()) { 18 | case Friendship.Type.Receive: 19 | if (config.acceptFriendKeyWords.length === 0) { 20 | console.log("无认证关键词,10秒后将会自动通过好友请求"); 21 | await delay(10000); 22 | await friendship.accept(); 23 | } else if ( 24 | config.acceptFriendKeyWords.length > 0 25 | && config.acceptFriendKeyWords.includes(hello) 26 | ) { 27 | console.log(`触发关键词${hello},10秒后自动通过好友请求`); 28 | await delay(10000); 29 | await friendship.accept(); 30 | } else { 31 | console.log("未触发任何关键词,好友自动添加失败"); 32 | } 33 | break; 34 | case Friendship.Type.Confirm: 35 | logMsg = `已确认添加好友:${name}`; 36 | console.log(logMsg); 37 | break; 38 | default: 39 | console.log(logMsg); 40 | break; 41 | } 42 | } else { 43 | console.log("未开启自动添加好友功能,忽略好友添加"); 44 | } 45 | } catch (e) { 46 | console.log("添加好友出错:", e); 47 | } 48 | } 49 | 50 | module.exports = onFriend; 51 | -------------------------------------------------------------------------------- /wechat/common/index.js: -------------------------------------------------------------------------------- 1 | const { FileBox, UrlLink, MiniProgram } = require('wechaty'); 2 | const { delay } = require('../../utils'); 3 | /** 4 | * 私聊发送消息 5 | * @param contact 6 | * @param msg 7 | * @param isRoom 8 | * type 1 文字 2 图片url 3 图片base64 4 url链接 5 小程序 6 名片 9 | */ 10 | async function contactSay(contact, msg, isRoom = false) { 11 | try { 12 | if (msg.type === 1 && msg.content) { 13 | // 文字 14 | console.log('回复内容', msg.content); 15 | await contact.say(msg.content); 16 | } else if (msg.type === 2 && msg.url) { 17 | // url文件 18 | const obj = FileBox.fromUrl(msg.url); 19 | await obj.ready(); 20 | console.log('回复内容', obj); 21 | if (isRoom) { 22 | await contact.say(`@${contact.name()}`); 23 | await delay(500); 24 | } 25 | await contact.say(obj); 26 | } else if (msg.type === 3 && msg.url) { 27 | // bse64文件 28 | const obj = FileBox.fromDataURL(msg.url, 'user-avatar.jpg'); 29 | await contact.say(obj); 30 | } else if (msg.type === 4 && msg.url && msg.title && msg.description && msg.thumbUrl) { 31 | const url = new UrlLink({ 32 | description: msg.description, 33 | thumbnailUrl: msg.thumbUrl, 34 | title: msg.title, 35 | url: msg.url, 36 | }); 37 | await contact.say(url); 38 | } else if (msg.type === 5 39 | && msg.appid 40 | && msg.title 41 | && msg.pagePath 42 | && msg.description 43 | && msg.thumbUrl 44 | && msg.thumbKey) { 45 | const miniProgram = new MiniProgram({ 46 | appid: msg.appid, 47 | title: msg.title, 48 | pagePath: msg.pagePath, 49 | description: msg.description, 50 | thumbUrl: msg.thumbUrl, 51 | thumbKey: msg.thumbKey, 52 | }); 53 | await contact.say(miniProgram); 54 | } 55 | } catch (e) { 56 | console.log('私聊发送消息失败', e); 57 | } 58 | } 59 | 60 | module.exports = { 61 | contactSay, 62 | }; 63 | -------------------------------------------------------------------------------- /wechat/service/msg-filter-service.js: -------------------------------------------------------------------------------- 1 | const msgFilter = require('./msg-filters'); 2 | const { checkOrder } = require('../../utils'); 3 | 4 | const WEIXINOFFICIAL = ['朋友推荐消息', '微信支付', '微信运动', '微信团队', 'recommendation message']; // 微信官方账户,针对此账户不做任何回复 5 | const DELETEFRIEND = '开启了朋友验证'; // 被人删除后,防止重复回复 6 | const REMINDKEY = '提醒'; 7 | const NEWADDFRIEND = '你已添加'; 8 | const SHOP_SITES = ['tb.cn', 'taobao.com', 'jd.com', 'pinduoduo.com', 'yangkeduo.com']; // 转链支持网站 9 | 10 | function isIncludes(arr, str) { 11 | let res = false; 12 | arr.forEach((item) => { 13 | if (str.includes(item)) { 14 | res = true; 15 | } 16 | }); 17 | return res; 18 | } 19 | 20 | async function getMsgReply(resArray, { 21 | that, msg, name, contact, avatar, id, room, 22 | }) { 23 | try { 24 | let msgArr = []; 25 | for (let i = 0; i < resArray.length; i += 1) { 26 | const item = resArray[i]; 27 | if (item.bool) { 28 | msgArr = (await msgFilter[item.method]({ 29 | that, msg, name, contact, avatar, id, room, 30 | })) || []; 31 | } 32 | if (msgArr.length > 0) { 33 | return msgArr; 34 | } 35 | } 36 | return []; 37 | } catch (e) { 38 | console.log('getMsgReply error', e); 39 | return []; 40 | } 41 | } 42 | 43 | /** 44 | * 微信好友文本消息事件过滤 45 | * 46 | * @param that wechaty实例 47 | * @param {Object} contact 发消息者信息 48 | * @param {string} msg 消息内容 49 | * @returns {number} 返回回复内容 50 | */ 51 | async function filterFriendMsg(that, contact, msg) { 52 | let msgArr = []; 53 | try { 54 | const name = contact.name(); 55 | const { id } = contact; 56 | const avatar = await contact.avatar(); 57 | const resArray = [ 58 | { bool: msg === '', method: 'emptyMsg' }, 59 | { bool: msg.includes(DELETEFRIEND) || WEIXINOFFICIAL.includes(name), method: 'officialMsg' }, 60 | { bool: msg.includes(NEWADDFRIEND), method: 'newFriendMsg' }, 61 | { bool: msg.startsWith(REMINDKEY), method: 'scheduleJobMsg' }, 62 | { bool: isIncludes(SHOP_SITES, msg), method: 'searchPromoteLink' }, 63 | { bool: checkOrder(msg), method: 'bindOrder' }, 64 | { bool: true, method: 'keywordsMsg' }, 65 | ]; 66 | msgArr = await getMsgReply(resArray, { 67 | that, msg, contact, name, avatar, id, 68 | }); 69 | } catch (e) { 70 | console.log('filterFriendMsg error', e); 71 | } 72 | return msgArr && msgArr.length > 0 ? msgArr : []; 73 | } 74 | 75 | module.exports = { 76 | filterFriendMsg, 77 | }; 78 | -------------------------------------------------------------------------------- /wechat/db_action/index.js: -------------------------------------------------------------------------------- 1 | // const moment = require('moment') 2 | const { MongoClient } = require('mongodb'); 3 | 4 | const con_url = "mongodb://localhost:27017/"; 5 | 6 | const connectMongo = (dbase_name) => MongoClient.connect(con_url).then((conn) => { 7 | console.log("数据库已连接"); 8 | const dbase = conn.db(dbase_name); 9 | return { 10 | conn, 11 | dbase, 12 | }; 13 | }).catch((err) => { 14 | console.log("数据库连接失败"); 15 | }); 16 | 17 | const insertData = (mongoObj, collection_name, data) => { 18 | const table = mongoObj.dbase.collection(collection_name); 19 | // 增加 20 | return table.insertOne(data).then((res) => { 21 | console.log("已插入数据成功"); 22 | }).catch((err) => { 23 | console.log("插入数据错误", err); 24 | }); 25 | }; 26 | 27 | const updateData = (mongoObj, collection_name, query, update_data) => { 28 | const table = mongoObj.dbase.collection(collection_name); 29 | // 修改 30 | return table.updateOne(query, { $set: update_data }).then((res) => { 31 | console.log("修改数据成功"); 32 | }).catch((err) => { 33 | console.log("修改数据错误", err); 34 | }); 35 | }; 36 | 37 | const selectData = (mongoObj, collection_name = '', data = null) => { 38 | const girls_table = mongoObj.dbase.collection(collection_name); 39 | // 查询 40 | return girls_table.find(data).toArray().then((res) => { 41 | console.log("查询数据成功"); 42 | return res; 43 | }).catch((err) => { 44 | console.log("查询数据错误", err); 45 | }); 46 | }; 47 | 48 | const insertCustomer = (mongoObj, data) => insertData(mongoObj, 'customers', data); 49 | const selectCustomers = (mongoObj, data) => selectData(mongoObj, 'customers', data); 50 | const updateCustomer = (mongoObj, query, data) => updateData(mongoObj, 'customers', query, data); 51 | const insertOrder = (mongoObj, data) => insertData(mongoObj, 'orders', data); 52 | const selectOrders = (mongoObj, data) => selectData(mongoObj, 'orders', data); 53 | const updateOrder = (mongoObj, query, data) => updateData(mongoObj, 'orders', query, data); 54 | const insertTurnLink = (mongoObj, data) => insertData(mongoObj, 'turnlinks', data); 55 | const selectTurnLinks = (mongoObj, data) => selectData(mongoObj, 'turnlinks', data); 56 | const updateTurnLink = (mongoObj, query, data) => updateData(mongoObj, 'turnlinks', query, data); 57 | 58 | async function test() { 59 | const mongoObj = await connectMongo('taobaoke'); 60 | const insterData = { name: 'xuhaha', age: 15 }; 61 | await insertData(mongoObj, 'users', insterData); 62 | } 63 | module.exports = { 64 | connectMongo, 65 | selectData, 66 | insertData, 67 | insertCustomer, 68 | selectCustomers, 69 | updateCustomer, 70 | insertOrder, 71 | selectOrders, 72 | updateOrder, 73 | insertTurnLink, 74 | selectTurnLinks, 75 | updateTurnLink, 76 | }; 77 | -------------------------------------------------------------------------------- /tbk-api-server/jd/README.md: -------------------------------------------------------------------------------- 1 | ## jd-union 2 | > 京东联盟Nodejs SDK 3 | 4 | ### NPM 5 | [ 6 | ![NPM version](https://img.shields.io/npm/v/jd-union.svg) 7 | ![NPM download](https://img.shields.io/npm/dm/jd-union.svg) 8 | ![NPM download](https://img.shields.io/npm/dw/jd-union.svg) 9 | ](https://www.npmjs.com/package/jd-union) 10 | 11 | ### 安装 12 | ``` 13 | npm i -save jd-union 14 | // or 15 | yarn add jd-union 16 | ``` 17 | 18 | ### 导入 19 | 整个导入 20 | ```js 21 | const JdUnion = require('jd-union'); 22 | ``` 23 | 24 | 按需导入 25 | ```js 26 | const {create, sign, request} = require('jd-union'); 27 | ``` 28 | 29 | ### 使用 30 | 创建Api实例 31 | ```js 32 | const {create} = require('jd-union'); 33 | 34 | const api = create( 35 | { 36 | appKey: '<联盟分配给应用的appKey>如:eefc33bDRea044cb8ctre5hycf0ac1934', 37 | appSecret: '<联盟分配给应用的appSecret>如:6d34r0d0kild46460654b42f5e350982', 38 | serverUrl: '默认为 https://api.jd.com/routerjson' 39 | } 40 | ); 41 | 42 | // 调用Api 43 | // api.request(API接口名称, 业务参数); 返回promise 44 | const res = await api.request( 45 | 'jd.union.open.goods.jingfen.query', 46 | { 47 | goodsReq: 110 48 | } 49 | ); 50 | 51 | // 验签 52 | const signature = api.sign( 53 | { 54 | method: 'jd.union.open.goods.query', 55 | param_json: {'goodsReqDTO': {'keyword': '男装', 'pageSize': 10, 'pageIndex': 1}}, // 0.2.x版本后,key为360buy_param_json或param_json皆可 56 | v: '1.0', 57 | access_token: '', 58 | timestamp: '2018-10-18 11:13:12', 59 | sign_method: 'md5', 60 | format: 'json' 61 | } 62 | ); 63 | 64 | console.log(signature); 65 | ``` 66 | 67 | 单独验签 68 | ```js 69 | const {sign} = require('jd-union'); 70 | 71 | const signature = sign( 72 | { 73 | method: 'jd.union.open.goods.query', 74 | param_json: {"goodsReqDTO":{"keyword":"男装","pageSize":10,"pageIndex":1}}, // 0.2.x版本后,key为360buy_param_json或param_json皆可 75 | v: '1.0', 76 | ccess_token: '', 77 | timestamp: '2018-10-18 11:13:12', 78 | sign_method: 'md5', 79 | format: 'json' 80 | }, 81 | 'eefc33bDRea044cb8ctre5hycf0ac1934', 82 | '6d34r0d0kild46460654b42f5e350982', 83 | 'https://api.jd.com/routerjson' 84 | ); 85 | 86 | console.log(signature); 87 | ``` 88 | 89 | 单独调用Api 90 | ```js 91 | const {request} = require('jd-union'); 92 | 93 | /* 94 | const res = await request('https://router.jd.com/api', 95 | API接口名称, 96 | 业务参数, 97 | Api版本号, 98 | access_token = '', 99 | '<联盟分配给应用的appKey>如:eefc33bDRea044cb8ctre5hycf0ac1934', 100 | '<联盟分配给应用的appSecret>如:6d34r0d0kild46460654b42f5e350982'); 101 | */ 102 | const res = await request( 103 | 'https://api.jd.com/routerjson', 104 | 'jd.union.open.goods.jingfen.query', 105 | { 106 | goodsReq: 110 107 | }, 108 | '1.0', 109 | '', 110 | 'eefc33bDRea044cb8ctre5hycf0ac1934', 111 | '6d34r0d0kild46460654b42f5e350982' 112 | ); 113 | ``` 114 | -------------------------------------------------------------------------------- /wechat/handlers/on-message.js: -------------------------------------------------------------------------------- 1 | const { contactSay } = require('../common'); 2 | const { getContactTextReply } = require('../common/reply'); 3 | const { delay, updateWechatFirend } = require('../../utils'); 4 | /** 5 | * 根据消息类型过滤私聊消息事件 6 | * @param {*} that bot实例 7 | * @param {*} msg 消息主体 8 | */ 9 | async function dispatchFriendFilterByMsgType(that, msg) { 10 | try { 11 | const type = msg.type(); 12 | const contact = msg.talker(); // 发消息人 13 | // console.log('联系人===========================》', contact) 14 | const isOfficial = contact.type() === that.Contact.Type.Official; 15 | let content = ''; 16 | let replys = []; 17 | const contactName = contact.name(); 18 | if (contactName.indexOf('recommendation message') == -1) { 19 | updateWechatFirend(contact); 20 | } 21 | switch (type) { 22 | case that.Message.Type.Text: 23 | content = msg.text(); 24 | if (!isOfficial) { 25 | console.log(`发消息人${contactName}:${content}`); 26 | if (content.trim()) { 27 | replys = await getContactTextReply(that, contact, content) || []; 28 | for (let i = 0; i < replys.length; i += 1) { 29 | const reply = replys[i]; 30 | await delay(1000); 31 | await contactSay(contact, reply); 32 | } 33 | } 34 | } else { 35 | console.log('公众号消息'); 36 | } 37 | break; 38 | case that.Message.Type.Emoticon: 39 | console.log(`发消息人${contactName}:发了一个表情`); 40 | break; 41 | case that.Message.Type.Image: 42 | console.log(`发消息人${contactName}:发了一张图片`); 43 | break; 44 | case that.Message.Type.Url: 45 | console.log(`发消息人${contactName}:发了一个链接`); 46 | break; 47 | case that.Message.Type.Video: 48 | console.log(`发消息人${contactName}:发了一个视频`); 49 | break; 50 | case that.Message.Type.Audio: 51 | console.log(`发消息人${contactName}:发了一个视频`); 52 | break; 53 | default: 54 | break; 55 | } 56 | } catch (error) { 57 | console.log('监听消息错误', error); 58 | } 59 | } 60 | 61 | async function onMessage(msg) { 62 | try { 63 | const room = msg.room(); // 是否为群消息 64 | // console.log('roomroomroomroom----------',room) 65 | const msgSelf = msg.self(); // 是否自己发给自己的消息 66 | if (msgSelf) return; 67 | if (room) { 68 | // 群消息 69 | const contact = msg.talker(); // 发消息人 70 | } else { 71 | await dispatchFriendFilterByMsgType(this, msg); 72 | } 73 | } catch (e) { 74 | console.log('监听消息失败', e); 75 | } 76 | } 77 | 78 | module.exports = onMessage; 79 | -------------------------------------------------------------------------------- /wechat/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WechatBot 3 | * - https://github.com/gengchen528/wechatBot 4 | */ 5 | const { WechatyBuilder } = require('wechaty'); 6 | const qrcodeTerminal = require('qrcode-terminal'); 7 | const schedule = require('../schedule/index'); 8 | const config = require('../config/index'); 9 | const onFriend = require('./handlers/on-friend'); 10 | const onMessage = require('./handlers/on-message'); 11 | 12 | // 延时函数,防止检测出类似机器人行为操作 13 | function delay(ms) { 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | resolve(); 17 | }, ms); 18 | }); 19 | } 20 | 21 | // 二维码生成 22 | function onScan(qrcode, status) { 23 | if (qrcode) { 24 | console.log(qrcode); 25 | const qrcodeImageUrl = [ 26 | 'https://wechaty.js.org/qrcode/', 27 | encodeURIComponent(qrcode), 28 | ].join(''); 29 | console.info('StarterBot', 'onScan: %s(%s) - %s', status, qrcodeImageUrl); 30 | 31 | qrcodeTerminal.generate(qrcode, { small: true }); // show qrcode on console 32 | console.info(`[${status}] ${qrcode}\nScan QR Code above to log in: `); 33 | } else { 34 | console.info(`[${status}]`); 35 | } 36 | } 37 | 38 | const bot = WechatyBuilder.build({ 39 | name: 'puppet-wechat', 40 | puppetOptions: { 41 | uos: true, // 开启uos协议 42 | }, 43 | puppet: 'wechaty-puppet-wechat', 44 | }); 45 | 46 | // 创建微信心跳任务 47 | async function initHeart() { 48 | schedule.setSchedule(config.heartService.SENDDATE, async () => { 49 | let logMsg; 50 | const contact = (await bot.Contact.find({ name: config.service.vx })) 51 | || (await bot.Contact.find({ alias: config.heartService.vx })); 52 | const str = 'alive'; 53 | try { 54 | logMsg = str; 55 | await delay(2000); 56 | await contact.say(str); // 发送消息 57 | } catch (e) { 58 | logMsg = e.message; 59 | } 60 | console.log(logMsg); 61 | }); 62 | } 63 | // 登录 64 | async function onLogin(user) { 65 | console.log(`淘宝客微信bot${user}登录了`); 66 | const date = new Date(); 67 | console.log(`当前容器时间:${date}`); 68 | await initHeart(); 69 | } 70 | 71 | // 登出 72 | function onLogout(user) { 73 | console.log(`淘宝客微信bot${user} 已经登出`); 74 | } 75 | 76 | async function onReady() { 77 | console.log('ready'); 78 | // const contactList = await bot.Contact.findAll() 79 | // console.info('Total number of contacts:', contactList.length) 80 | // for (let i = 0; i < contactList.length; i++) { 81 | // const contact = contactList[i] 82 | // console.log('contact',contact) 83 | // if (contact.type()==bot.Contact.Type.Individual) { 84 | // await utils.updateWechatFirend(contact) 85 | // } 86 | // } 87 | } 88 | 89 | bot.on('scan', onScan); 90 | bot.on('login', onLogin); 91 | bot.on('logout', onLogout); 92 | bot.on('message', onMessage); 93 | bot.on('friendship', onFriend); 94 | bot.on('ready', onReady); 95 | 96 | bot 97 | .start() 98 | .then(() => console.log('开始登陆微信')) 99 | .catch((e) => console.error(e)); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tbk-淘宝客返利系统(nodejs v16) 2 | 3 | ## 现有功能 4 | 5 | * 支持 淘宝、京东、拼多多转链([转链服务源码](/tbk-api-server/README.md)) ([api测试链接](https://tbk-api.xumeng.host/?url=%E3%80%90%E6%B7%98%E5%AE%9D%E3%80%91https://m.tb.cn/h.UKRgdJO?tk=Aa0UdRum9Sd%20CZ3457%20%E3%80%8C%E4%B8%80%E6%AC%A1%E6%80%A7%E9%A5%BA%E5%AD%90%E7%9B%92%E5%A4%96%E5%8D%96%E4%B8%93%E7%94%A8%E9%A4%90%E7%9B%92%E5%95%86%E7%94%A8%E9%80%9F%E5%86%BB%E6%B0%B4%E9%A5%BA%E7%9B%92%E9%A6%84%E9%A5%A8%E6%89%93%E5%8C%85%E7%9B%92%E5%88%86%E6%A0%BC%E5%8C%85%E8%A3%85%E7%9B%92%E5%AD%90%E3%80%8D%20%E7%82%B9%E5%87%BB%E9%93%BE%E6%8E%A5%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%20%E6%88%96%E8%80%85%20%E6%B7%98%E5%AE%9D%E6%90%9C%E7%B4%A2%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80)) 6 | * 微信bot(是微信,不是公众号,当然公众号也实现了的)自动回复消息 转链、绑定订单(基于wechaty) ([微信bot源码](/wechat/README.md)) 7 | * 返利系统订单管理后台(vue2、element-ui、express、mongoose...)(已开源) ([管理后台](https://github.com/pea-cake/tbk-manage.git)) 8 | * ... 9 | 10 | ## 如何运行和部署 11 | 12 | ### 运行 13 | 14 | 1. 配置(./config/index.js) 阿里联盟、京东联盟、多多客 返佣账号appkey appsecret 15 | 16 | ```text 17 | JDconfig: { 18 | // 京东联盟 19 | appKey: "", 20 | appSecret: "", 21 | }, 22 | TBconfig: { 23 | // 阿里联盟, 获取配置看这里https://github.com/pea-cake/tbk/issues/5 24 | appkey: "", 25 | appsecret: "", 26 | adzone_id: "", // 推广位pid 'mm_123_456_789' 的789就是adzone_id 27 | }, 28 | PDDconfig: { 29 | // 多多客 30 | clientId: "", 31 | clientSecret: "", 32 | pid: "", // 推广位pid 33 | } 34 | ``` 35 | 36 | 2. 安装mongodb数据库(自行查找方法) 37 | 38 | 3. 安装node环境(自行查找方法) 39 | 40 | 4. 安装所需包 41 | 42 | ```bash 43 | npm i 44 | ``` 45 | 46 | 5. 运行转链api服务 47 | 48 | ```bash 49 | npm run server 50 | ``` 51 | 52 | 6. 另启动一个终端,运行微信bot服务 53 | 54 | ```bash 55 | npm run wechat 56 | ``` 57 | 58 | 7. 扫码登录微信即可 59 | 8. 还可使用pm2 运行 60 | 61 | ```bash 62 | npm install pm2 -g 63 | pm2 start tbk-api-server/index.js 64 | pm2 start wechat/index.js 65 | ``` 66 | 67 | ### 部署 68 | 69 | ## docker 70 | 71 | 1. 已经安装docker 72 | 2. 完成配置(config/index.js) 73 | 3. 74 | * DockerFile 75 | 76 | ```bash 77 | docker build -t tbk:v1 . 78 | docker run -it tbk:v1 /bin/bash 79 | ``` 80 | 81 | * docker远程仓库 82 | 83 | ```bash 84 | docker pull peacaker/tbk:1.0.0 85 | docker run -it peacaker/tbk:1.0.0 /bin/bash 86 | ``` 87 | 88 | 4. 扫码登录微信即可 89 | 90 | ## 🧐🧐🧐 91 | 92 | * 这是一个能赚点小钱,即使不能赚钱,也能方便你省钱的系统 93 | * 已全部开源,欢迎使用,欢迎star,也期待你的添砖加瓦 94 | * ... 95 | 96 | ## 体验 97 | 1. 转链api体验:https://tbk-api.xumeng.host/?url=商品链接. 98 | 2. 微信扫码(风控暂时停用,可能由于玩chatgpt多了): 99 | 100 | 101 | ## 展示 102 | 103 | 微信消息部分: 104 | 105 | 106 | 管理系统截图: 107 | 108 | ![image](https://user-images.githubusercontent.com/58544092/197322244-3db634f8-fdce-491c-8339-6ea9bdfdab75.png) 109 | 110 | ![image](https://user-images.githubusercontent.com/58544092/197322205-74d8f0e6-9798-43c9-af7b-a567a3144fde.png) 111 | ... 112 | 113 | ## 感谢 114 | 115 | * 待添加 116 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/topUtil.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var util = require('util'); 3 | var stream = require('stream'); 4 | 5 | /** 6 | * hash 7 | * 8 | * @param {String} method hash method, e.g.: 'md5', 'sha1' 9 | * @param {String|Buffer} s 10 | * @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'. 11 | * @return {String} md5 hash string 12 | * @public 13 | */ 14 | exports.hash = function hash(method, s, format) { 15 | var sum = crypto.createHash(method); 16 | var isBuffer = Buffer.isBuffer(s); 17 | if (!isBuffer && typeof s === 'object') { 18 | s = JSON.stringify(sortObject(s)); 19 | } 20 | sum.update(s, isBuffer ? 'binary' : 'utf8'); 21 | return sum.digest(format || 'hex'); 22 | }; 23 | 24 | /** 25 | * md5 hash 26 | * 27 | * @param {String|Buffer} s 28 | * @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'. 29 | * @return {String} md5 hash string 30 | * @public 31 | */ 32 | exports.md5 = function md5(s, format) { 33 | return exports.hash('md5', s, format); 34 | }; 35 | 36 | exports.YYYYMMDDHHmmss = function (d, options) { 37 | d = d || new Date(); 38 | if (!(d instanceof Date)) { 39 | d = new Date(d); 40 | } 41 | 42 | var dateSep = '-'; 43 | var timeSep = ':'; 44 | if (options) { 45 | if (options.dateSep) { 46 | dateSep = options.dateSep; 47 | } 48 | if (options.timeSep) { 49 | timeSep = options.timeSep; 50 | } 51 | } 52 | var date = d.getDate(); 53 | if (date < 10) { 54 | date = '0' + date; 55 | } 56 | var month = d.getMonth() + 1; 57 | if (month < 10) { 58 | month = '0' + month; 59 | } 60 | var hours = d.getHours(); 61 | if (hours < 10) { 62 | hours = '0' + hours; 63 | } 64 | var mintues = d.getMinutes(); 65 | if (mintues < 10) { 66 | mintues = '0' + mintues; 67 | } 68 | var seconds = d.getSeconds(); 69 | if (seconds < 10) { 70 | seconds = '0' + seconds; 71 | } 72 | return d.getFullYear() + dateSep + month + dateSep + date + ' ' + 73 | hours + timeSep + mintues + timeSep + seconds; 74 | }; 75 | 76 | exports.checkRequired = function (params, keys) { 77 | if (!Array.isArray(keys)) { 78 | keys = [keys]; 79 | } 80 | for (var i = 0, l = keys.length; i < l; i++) { 81 | var k = keys[i]; 82 | if (!params.hasOwnProperty(k)) { 83 | var err = new Error('`' + k + '` required'); 84 | err.name = "ParameterMissingError"; 85 | return err; 86 | } 87 | } 88 | }; 89 | 90 | exports.getApiResponseName = function(apiName){ 91 | var reg = /\./g; 92 | if(apiName.match("^taobao")) 93 | apiName = apiName.substr(7); 94 | return apiName.replace(reg,'_')+"_response"; 95 | } 96 | 97 | exports.getLocalIPAdress = function (){ 98 | var interfaces = require('os').networkInterfaces(); 99 | for(var devName in interfaces){ 100 | var iface = interfaces[devName]; 101 | for(var i=0;i -1; 10 | } 11 | 12 | /** 13 | * 校验SPI请求签名,不支持带上传文件的HTTP请求。 14 | * 15 | * @param bizParams 业务参数 16 | * @param httpHeaders http头部信息 17 | * @param secret APP密钥 18 | * @param charset 目标编码 19 | * @return boolean 20 | */ 21 | exports.checkSignForSpi = function checkSignForSpi(url,body,httpHeaders,secret) { 22 | var ctype = httpHeaders['content-type']; 23 | if(!ctype){ 24 | ctype = httpHeaders['Content-Type']; 25 | } 26 | if(!ctype){ 27 | return false; 28 | } 29 | 30 | var charset = this.getResponseCharset(ctype); 31 | var urlParams = URL.parse(url).query.split("&"); 32 | var bizParams = buildBizParams(urlParams); 33 | return checkSignInternal(bizParams,body,httpHeaders,secret,charset); 34 | } 35 | 36 | function buildBizParams(urlParams){ 37 | var bizParams = {}; 38 | for(var i =0; i < urlParams.length; i++){ 39 | var params = urlParams[i].split("="); 40 | bizParams[params[0]] = params[1]; 41 | } 42 | return bizParams; 43 | } 44 | 45 | /** 46 | * 检查发起SPI请求的来源IP是否是TOP机房的出口IP。 47 | * 48 | * @param request HTTP请求 49 | * @param topIpList TOP网关IP出口地址段列表,通过taobao.top.ipout.get获得 50 | * 51 | * @return boolean true表达IP来源合法,false代表IP来源不合法 52 | */ 53 | exports.checkRemoteIp = function checkRemoteIp(httpHeaders,topIpList){ 54 | var ip = null; 55 | for(var i = 0; i < ipFileds.length; i++){ 56 | var realIp = httpHeaders[ipFileds[i]]; 57 | if(realIp && 'unknown' != realIp.toLowerCase()){ 58 | ip = realIp; 59 | break; 60 | } 61 | } 62 | 63 | if(ip){ 64 | for(var i = 0; i < topIpList.length; i++) { 65 | if(ip == topIpList[i]){ 66 | return true; 67 | } 68 | } 69 | } 70 | return false; 71 | } 72 | 73 | /** 74 | * 检查SPI请求到达服务器端是否已经超过指定的分钟数,如果超过则拒绝请求。 75 | * 76 | * @return boolean true代表不超过,false代表超过。 77 | */ 78 | exports.checkTimestamp = function checkTimestamp(bizParams,minutes){ 79 | var timestamp = bizParams['timestamp']; 80 | if(timestamp){ 81 | var remove = new Date(timestamp).getTime(); 82 | var local = new Date().getTime(); 83 | return (local - remove) <= minutes * 60 * 1000; 84 | } 85 | return false; 86 | } 87 | 88 | function arrayConcat(bizParams,signHttpParams){ 89 | if(signHttpParams){ 90 | for(var i=0; i < signHttpParams.length; i++){ 91 | bizParams[signHttpParams[i].key] = signHttpParams[i].value; 92 | } 93 | } 94 | } 95 | 96 | function checkSignInternal(bizParams,body,httpHeaders,secret,charset){ 97 | var remoteSign = bizParams['sign']; 98 | arrayConcat(bizParams,getHeaderMap(httpHeaders)); 99 | var sorted = Object.keys(bizParams).sort(); 100 | var bastString = secret; 101 | var localSign ; 102 | for (var i = 0, l = sorted.length; i < l; i++) { 103 | var k = sorted[i]; 104 | var value = bizParams[k]; 105 | if(k == 'sign'){ 106 | continue; 107 | } 108 | value = urlencode.decode(bizParams[k],charset); 109 | 110 | if(k == 'timestamp'){ 111 | value = value.replace('+',' '); 112 | } 113 | k = iconv.encode(k,charset); 114 | bastString += k; 115 | bastString += value; 116 | } 117 | if(body){ 118 | bastString += body; 119 | } 120 | 121 | bastString += secret; 122 | var buffer = iconv.encode(bastString,charset); 123 | localSign = util.md5(buffer).toUpperCase(); 124 | return localSign == remoteSign; 125 | } 126 | 127 | function getHeaderMap(httpHeaders){ 128 | var resultMap = {}; 129 | var signList = httpHeaders['top-sign-list']; 130 | if(signList){ 131 | var targetKeys = signList.split(","); 132 | targetKeys.forEach(function(target){ 133 | resultMap[target] = httpHeaders[target]; 134 | }) 135 | } 136 | return resultMap; 137 | } 138 | 139 | exports.getResponseCharset = function getResponseCharset(ctype){ 140 | var charset = 'UTF-8'; 141 | if(ctype){ 142 | var params = ctype.split(";"); 143 | for(var i = 0; i < params.length; i++){ 144 | var param = params[i].trim(); 145 | if(param.startsWith('charset')){ 146 | var pair = param.split("="); 147 | charset = pair[1].trim().toUpperCase(); 148 | } 149 | } 150 | } 151 | if(charset && charset.toLowerCase().startsWith('GB')){ 152 | charset = "GBK"; 153 | } 154 | return charset; 155 | } 156 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/api/dingtalkClient.js: -------------------------------------------------------------------------------- 1 | var util = require('../topUtil.js'); 2 | var RestClient = require('./network.js') 3 | var Stream = require('stream') 4 | 5 | /** 6 | * Dingtalk API Client. 7 | * 8 | * @param {Object} options. 9 | * @constructor 10 | */ 11 | 12 | function DingtalkClient(options) { 13 | if (!(this instanceof DingtalkClient)) { 14 | return new DingtalkClient(options); 15 | } 16 | options = options || {}; 17 | this.url = options.url || 'https://eco.taobao.com/router/rest'; 18 | } 19 | 20 | /** 21 | * Invoke an api by method name. 22 | * 23 | * @param {String} method, method name 24 | * @param {Object} params 25 | * @param {Array} reponseNames, e.g. ['tmall_selected_items_search_response', 'tem_list', 'selected_item'] 26 | * @param {Object} defaultResponse 27 | * @param {Function(err, response)} callback 28 | */ 29 | DingtalkClient.prototype.invoke = function (type,method, params,reponseNames, callback) { 30 | params.method = method; 31 | this.request(type,params,function (err, result) { 32 | if (err) { 33 | return callback(err); 34 | } 35 | var response = result; 36 | if (reponseNames && reponseNames.length > 0) { 37 | for (var i = 0; i < reponseNames.length; i++) { 38 | var name = reponseNames[i]; 39 | response = response[name]; 40 | if (response === undefined) { 41 | break; 42 | } 43 | } 44 | } 45 | callback(null, response); 46 | }); 47 | }; 48 | 49 | /** 50 | * Request API. 51 | * 52 | * @param {Object} params 53 | * @param {String} [type='GET'] 54 | * @param {Function(err, result)} callback 55 | * @public 56 | */ 57 | DingtalkClient.prototype.request = function (type,params,callback) { 58 | var err = util.checkRequired(params, 'method'); 59 | if (err) { 60 | return callback(err); 61 | } 62 | var args = { 63 | timestamp: this.timestamp(), 64 | format: 'json', 65 | v: '2.0', 66 | sign_method: 'md5' 67 | }; 68 | 69 | var request = null; 70 | if(type == 'get'){ 71 | request = RestClient.get(this.url); 72 | }else{ 73 | request = RestClient.post(this.url); 74 | } 75 | 76 | for (var key in params) { 77 | if(typeof params[key] === 'object' && Buffer.isBuffer(params[key])){ 78 | request.attach(key,params[key],{knownLength:params[key].length,filename:key}) 79 | } else if(typeof params[key] === 'object' && Stream.Readable(params[key]) && !util.is(params[key]).a(String)){ 80 | request.attach(key, params[key]); 81 | } else if(typeof params[key] === 'object'){ 82 | args[key] = JSON.stringify(params[key]); 83 | } else{ 84 | args[key] = params[key]; 85 | } 86 | } 87 | 88 | args.sign = this.sign(args); 89 | for(var key in args){ 90 | request.field(key, args[key]); 91 | } 92 | 93 | request.end(function(response){ 94 | if(response.statusCode == 200){ 95 | var data = response.body; 96 | var errRes = data && data.error_response; 97 | if (errRes) { 98 | callback(errRes, data); 99 | }else{ 100 | callback(err, data); 101 | } 102 | }else{ 103 | err = new Error('NetWork-Error'); 104 | err.name = 'NetWork-Error'; 105 | err.code = 15; 106 | err.sub_code = response.statusCode; 107 | callback(err, null); 108 | } 109 | }) 110 | }; 111 | 112 | /** 113 | * Get now timestamp with 'yyyy-MM-dd HH:mm:ss' format. 114 | * @return {String} 115 | */ 116 | DingtalkClient.prototype.timestamp = function () { 117 | return util.YYYYMMDDHHmmss(); 118 | }; 119 | 120 | /** 121 | * Sign API request. 122 | * see http://open.taobao.com/doc/detail.htm?id=111#s6 123 | * 124 | * @param {Object} params 125 | * @return {String} sign string 126 | */ 127 | DingtalkClient.prototype.sign = function (params) { 128 | var sorted = Object.keys(params).sort(); 129 | var basestring = this.appsecret; 130 | for (var i = 0, l = sorted.length; i < l; i++) { 131 | var k = sorted[i]; 132 | basestring += k + params[k]; 133 | } 134 | basestring += this.appsecret; 135 | return util.md5(basestring).toUpperCase(); 136 | }; 137 | 138 | /** 139 | * execute top api 140 | */ 141 | DingtalkClient.prototype.execute = function (apiname,params,callback) { 142 | this.invoke('post',apiname, params, [util.getApiResponseName(apiname)], callback); 143 | }; 144 | 145 | DingtalkClient.prototype.get = function (apiname,params,callback) { 146 | this.invoke('get',apiname, params, [util.getApiResponseName(apiname)], callback); 147 | }; 148 | 149 | exports.DingtalkClient = DingtalkClient; 150 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/tmc/tmcClient.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('ws'); 2 | var Common = require('./common.js').Common; 3 | var TmcCodec = require('./tmcCodec.js').TmcCodec; 4 | var util = require('../topUtil.js'); 5 | 6 | var codec = new TmcCodec(); 7 | var client ; 8 | var TmcClient = function TmcClient(appkey,appsecret,groupName) { 9 | this._appkey = appkey; 10 | this._appsecret = appsecret; 11 | this._groupName = groupName; 12 | this._uri = 'ws://mc.api.taobao.com/'; 13 | this._ws = null; 14 | this.isReconing = false; 15 | this._callback = null; 16 | this._interval = null; 17 | client = this; 18 | } 19 | 20 | 21 | TmcClient.prototype.createSign = function(timestamp){ 22 | var basestring = this._appsecret; 23 | basestring += 'app_key' + this._appkey; 24 | basestring += 'group_name' + this._groupName; 25 | basestring += 'timestamp' + timestamp; 26 | basestring += this._appsecret; 27 | return util.md5(basestring).toUpperCase(); 28 | } 29 | 30 | TmcClient.prototype.createConnectMessage = function() { 31 | var msg = {}; 32 | msg.messageType = Common.enum.MessageType.CONNECT; 33 | var timestamp = Date.now(); 34 | var content = { 35 | 'app_key':this._appkey, 36 | 'group_name':this._groupName, 37 | 'timestamp':timestamp+'', 38 | 'sign':this.createSign(timestamp), 39 | 'sdk':'NodeJS-1.2.0', 40 | 'intranet_ip':util.getLocalIPAdress() 41 | }; 42 | msg.content = content; 43 | var buffer = codec.writeMessage(msg); 44 | return buffer; 45 | } 46 | 47 | TmcClient.prototype.createPullMessage = function() { 48 | var msg = {}; 49 | msg.protocolVersion = 2; 50 | msg.messageType = Common.enum.MessageType.SEND; 51 | var content = { 52 | '__kind':Common.enum.MessageKind.PullRequest 53 | }; 54 | msg.token = client._token; 55 | msg.content = content; 56 | var buffer = codec.writeMessage(msg); 57 | return buffer; 58 | } 59 | 60 | TmcClient.prototype.createConfirmMessage = function(id) { 61 | var msg = {}; 62 | msg.protocolVersion = 2; 63 | msg.messageType = Common.enum.MessageType.SEND; 64 | var content = { 65 | '__kind':Common.enum.MessageKind.Confirm, 66 | 'id':id 67 | }; 68 | msg.token = client._token; 69 | msg.content = content; 70 | var buffer = codec.writeMessage(msg); 71 | return buffer; 72 | } 73 | 74 | TmcClient.prototype.autoPull = function () { 75 | if(client._ws){ 76 | client._ws.send(client.createPullMessage(), { binary: true, mask: true }); 77 | } 78 | } 79 | 80 | TmcClient.prototype.reconnect = function (duration) { 81 | if(this.isReconing) 82 | return; 83 | 84 | this.isReconing = true; 85 | setTimeout(function timeout() { 86 | client.connect(client._uri,client._callback); 87 | }, duration); 88 | } 89 | 90 | TmcClient.prototype.connect = function(uri,callback) { 91 | this._uri = uri; 92 | this._callback = callback; 93 | 94 | if(client._ws != null){ 95 | client._ws.close(); 96 | } 97 | 98 | var ws = new WebSocket(this._uri); 99 | 100 | ws.on('open', function open() { 101 | client._ws = ws; 102 | this.send(client.createConnectMessage(), { binary: true, mask: true }); 103 | if(!client._interval){ 104 | client._interval = setInterval(client.autoPull, 5000); 105 | } 106 | }); 107 | 108 | ws.on('message', function(data, flags) { 109 | if(flags.binary){ 110 | var message = codec.readMessage(data); 111 | if(message != null && message.messageType == Common.enum.MessageType.CONNECTACK){ 112 | if(message.statusCode){ 113 | throw new Error(message.statusPhase); 114 | }else{ 115 | client._token = message.token; 116 | console.log("top message channel connect success, token = "+message.token); 117 | } 118 | }else if(message != null && message.messageType == Common.enum.MessageType.SEND){ 119 | var status = {success:true}; 120 | try { 121 | client._callback(message,status); 122 | }catch (err) { 123 | status.success = false; 124 | } 125 | if(status.success){ 126 | ws.send(client.createConfirmMessage(message.id), { binary: true, mask: true }); 127 | } 128 | }else{ 129 | console.log(message); 130 | } 131 | } 132 | }); 133 | 134 | ws.on('ping',function(data, flags) { 135 | ws.pong(data,{mask: true },true); 136 | }); 137 | 138 | ws.on('error',function(reason, errorCode) { 139 | console.log('tmc client error,reason : '+ reason + ' code : '+ errorCode); 140 | console.log('tmc client channel closed begin reconnect'); 141 | client._ws = null; 142 | client.reconnect(15000); 143 | }); 144 | 145 | ws.on('close', function close() { 146 | console.log('tmc client channel closed begin reconnect'); 147 | client._ws = null; 148 | client.reconnect(3000); 149 | }); 150 | this.isReconing = false; 151 | } 152 | 153 | exports.TmcClient = TmcClient; -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/api/topClient.js: -------------------------------------------------------------------------------- 1 | var util = require('../topUtil.js'); 2 | var RestClient = require('./network.js') 3 | var Stream = require('stream') 4 | 5 | /** 6 | * TOP API Client. 7 | * 8 | * @param {Object} options, must set `appkey` and `appsecret`. 9 | * @constructor 10 | */ 11 | 12 | function TopClient(options) { 13 | if (!(this instanceof TopClient)) { 14 | return new TopClient(options); 15 | } 16 | options = options || {}; 17 | if (!options.appkey || !options.appsecret) { 18 | throw new Error('appkey or appsecret need!'); 19 | } 20 | this.url = options.url || 'http://gw.api.taobao.com/router/rest'; 21 | this.appkey = options.appkey; 22 | this.appsecret = options.appsecret; 23 | } 24 | 25 | /** 26 | * Invoke an api by method name. 27 | * 28 | * @param {String} method, method name 29 | * @param {Object} params 30 | * @param {Array} reponseNames, e.g. ['tmall_selected_items_search_response', 'tem_list', 'selected_item'] 31 | * @param {Object} defaultResponse 32 | * @param {Function(err, response)} callback 33 | */ 34 | TopClient.prototype.invoke = function (type,method, params,reponseNames,httpHeaders,callback) { 35 | params.method = method; 36 | this.request(type,params,httpHeaders,function (err, result) { 37 | if (err) { 38 | return callback(err); 39 | } 40 | var response = result; 41 | if (reponseNames && reponseNames.length > 0) { 42 | for (var i = 0; i < reponseNames.length; i++) { 43 | var name = reponseNames[i]; 44 | response = response[name]; 45 | if (response === undefined) { 46 | break; 47 | } 48 | } 49 | } 50 | callback(null, response); 51 | }); 52 | }; 53 | 54 | /** 55 | * Request API. 56 | * 57 | * @param {Object} params 58 | * @param {String} [type='GET'] 59 | * @param {Function(err, result)} callback 60 | * @public 61 | */ 62 | TopClient.prototype.request = function (type,params,httpHeaders,callback) { 63 | var err = util.checkRequired(params, 'method'); 64 | if (err) { 65 | return callback(err); 66 | } 67 | var args = { 68 | timestamp: this.timestamp(), 69 | format: 'json', 70 | app_key: this.appkey, 71 | v: '2.0', 72 | sign_method: 'md5' 73 | }; 74 | 75 | var request = null; 76 | if(type == 'get'){ 77 | request = RestClient.get(this.url); 78 | }else{ 79 | request = RestClient.post(this.url); 80 | } 81 | 82 | for (var key in params) { 83 | if(typeof params[key] === 'object' && Buffer.isBuffer(params[key])){ 84 | request.attach(key,params[key],{knownLength:params[key].length,filename:key}) 85 | } else if(typeof params[key] === 'object' && Stream.Readable(params[key]) && !util.is(params[key]).a(String)){ 86 | request.attach(key, params[key]); 87 | } else if(typeof params[key] === 'object'){ 88 | args[key] = JSON.stringify(params[key]); 89 | } else{ 90 | args[key] = params[key]; 91 | } 92 | } 93 | 94 | args.sign = this.sign(args); 95 | 96 | for(var key in httpHeaders) { 97 | request.header(key,httpHeaders[key]); 98 | } 99 | 100 | for(var key in args){ 101 | request.field(key, args[key]); 102 | } 103 | 104 | request.end(function(response){ 105 | if(response.statusCode == 200){ 106 | var data = response.body; 107 | var errRes = data && data.error_response; 108 | if (errRes) { 109 | callback(errRes, data); 110 | }else{ 111 | callback(err, data); 112 | } 113 | }else{ 114 | err = new Error('NetWork-Error'); 115 | err.name = 'NetWork-Error'; 116 | err.code = 15; 117 | err.sub_code = response.statusCode; 118 | callback(err, null); 119 | } 120 | }) 121 | }; 122 | 123 | /** 124 | * Get now timestamp with 'yyyy-MM-dd HH:mm:ss' format. 125 | * @return {String} 126 | */ 127 | TopClient.prototype.timestamp = function () { 128 | return util.YYYYMMDDHHmmss(); 129 | }; 130 | 131 | /** 132 | * Sign API request. 133 | * see http://open.taobao.com/doc/detail.htm?id=111#s6 134 | * 135 | * @param {Object} params 136 | * @return {String} sign string 137 | */ 138 | TopClient.prototype.sign = function (params) { 139 | var sorted = Object.keys(params).sort(); 140 | var basestring = this.appsecret; 141 | for (var i = 0, l = sorted.length; i < l; i++) { 142 | var k = sorted[i]; 143 | basestring += k + params[k]; 144 | } 145 | basestring += this.appsecret; 146 | return util.md5(basestring).toUpperCase(); 147 | }; 148 | 149 | /** 150 | * execute top api 151 | */ 152 | TopClient.prototype.execute = function (apiname,params,callback) { 153 | this.invoke('post',apiname, params, [util.getApiResponseName(apiname)], [], callback); 154 | }; 155 | 156 | TopClient.prototype.executeWithHeader = function (apiname,params,httpHeaders,callback) { 157 | this.invoke('post',apiname, params, [util.getApiResponseName(apiname)], httpHeaders || [], callback); 158 | }; 159 | 160 | TopClient.prototype.get = function (apiname,params,callback) { 161 | this.invoke('get',apiname, params, [util.getApiResponseName(apiname)], [], callback); 162 | }; 163 | 164 | exports.TopClient = TopClient; 165 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/tmc/tmcCodec.js: -------------------------------------------------------------------------------- 1 | var Common = require('./common.js').Common; 2 | 3 | var TmcCodec = function(){ 4 | 5 | } 6 | 7 | TmcCodec.prototype.writeMessage = function(message) { 8 | var buffer = new Buffer(256); 9 | buffer.writeUInt8(2,0); 10 | buffer.writeUInt8(message.messageType,1); 11 | var index = 2; 12 | 13 | if(message.statusCode && message.statusCode > 0){ 14 | buffer.writeUInt16LE(Common.enum.HeaderType.StatusCode,index); 15 | buffer.writeUInt32LE(message.statusCode,index+2); 16 | index += 6; 17 | } 18 | 19 | if(message.flag && message.flag > 0){ 20 | buffer.writeUInt16LE(Common.enum.HeaderType.Flag,index); 21 | buffer.writeUInt32LE(message.flag,index+2); 22 | index += 6; 23 | } 24 | 25 | if(message.token){ 26 | buffer.writeUInt16LE(Common.enum.HeaderType.Token,index); 27 | var length = Buffer.byteLength(message.token); 28 | buffer.writeUInt32LE(length,index+2); 29 | buffer.write(message.token,index+6,'UTF-8'); 30 | index = index + length + 6; 31 | } 32 | 33 | if(message.content){ 34 | for(var key in message.content){ 35 | buffer.writeUInt16LE(Common.enum.HeaderType.Custom,index); 36 | var length = Buffer.byteLength(key); 37 | buffer.writeUInt32LE(length,index+2); 38 | buffer.write(key,index+6,'UTF-8'); 39 | index = index + length + 6; 40 | 41 | length = Buffer.byteLength(message.content[key]); 42 | if(length == 0){ 43 | buffer.writeUInt8(Common.enum.ValueFormat.Void,index); 44 | }else{ 45 | var type = typeof message.content[key]; 46 | if(key == '__kind'){ 47 | buffer.writeUInt8(Common.enum.ValueFormat.Byte,index); 48 | buffer.writeUInt8(message.content[key],index+1); 49 | index += 2; 50 | } else if(type == 'number'){ 51 | buffer.writeUInt8(Common.enum.ValueFormat.Int64,index); 52 | const big = ~~(message.content[key] / (0xFFFFFFFF + 1)); 53 | const low = (message.content[key] % (0xFFFFFFFF + 1)); 54 | buffer.writeUInt32LE(low,index + 1); 55 | buffer.writeUInt32LE(big,index + 5); 56 | index += 9; 57 | } else{ 58 | buffer.writeUInt8(Common.enum.ValueFormat.CountedString,index); 59 | buffer.writeUInt32LE(length,index+1); 60 | buffer.write(message.content[key],index+5,'UTF-8'); 61 | index = index + length + 5; 62 | } 63 | } 64 | } 65 | } 66 | buffer.writeUInt16LE(Common.enum.HeaderType.EndOfHeaders,index); 67 | return buffer.slice(0,index+2); 68 | } 69 | 70 | TmcCodec.prototype.readMessage = function(buffer) { 71 | var message = {}; 72 | message.protocolVersion = buffer.readUInt8(0); 73 | message.messageType = buffer.readUInt8(1); 74 | try{ 75 | var headerType = buffer.readUInt16LE(2); 76 | var index = 4; 77 | while(headerType != Common.enum.HeaderType.EndOfHeaders){ 78 | if(headerType === Common.enum.HeaderType.StatusCode){ 79 | message.statusCode = buffer.readUInt32LE(index); 80 | index += 4; 81 | } else if(headerType === Common.enum.HeaderType.StatusPhrase){ 82 | var length = buffer.readUInt32LE(index); 83 | message.statusPhase = buffer.toString('UTF-8',index+4,index+length+4); 84 | index = index + length + 4; 85 | } else if(headerType === Common.enum.HeaderType.Flag){ 86 | message.flag = buffer.readUInt32LE(index); 87 | index += 4; 88 | } else if(headerType === Common.enum.HeaderType.Token){ 89 | var length = buffer.readUInt32LE(index); 90 | message.token = buffer.toString('UTF-8',index+4,index+length+4); 91 | index = index + length + 4; 92 | } else if(headerType === Common.enum.HeaderType.Custom){ 93 | var length = buffer.readUInt32LE(index); 94 | var key = buffer.toString('UTF-8',index+4,index+length+4); 95 | index = index + length + 4; 96 | 97 | var format = buffer.readUInt8(index); 98 | index += 1; 99 | if(format == Common.enum.ValueFormat.Int64 || format == Common.enum.ValueFormat.Date){ 100 | message[key] = buffer.readUInt32LE(index) + buffer.readUInt32LE(index+4) * 4294967296; 101 | index += 8; 102 | }else if(format == Common.enum.ValueFormat.CountedString){ 103 | length = buffer.readUInt32LE(index); 104 | message[key] = buffer.toString('UTF-8',index+4,index+length+4); 105 | index = index + length + 4; 106 | }else if(format == Common.enum.ValueFormat.Byte){ 107 | message[key] = buffer.readUInt8(index); 108 | index += 1; 109 | }else if(format == Common.enum.ValueFormat.Int32){ 110 | message[key] = buffer.readUInt32LE(index); 111 | index += 4; 112 | }else if(format == Common.enum.ValueFormat.Int16){ 113 | message[key] = buffer.readUInt16LE(index); 114 | index += 2; 115 | } 116 | } 117 | headerType = buffer.readUInt16LE(index); 118 | index += 2; 119 | } 120 | }catch (err) { 121 | console.log(err); 122 | return null; 123 | } 124 | return message; 125 | } 126 | 127 | exports.TmcCodec = TmcCodec; 128 | 129 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const config = require('../config'); 3 | const db_action = require('../wechat/db_action'); 4 | 5 | function getHttpString(s) { 6 | let res = ''; 7 | const reg = /(https?|http|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g; 8 | if (s) { 9 | res = s.match(reg) || ''; 10 | } 11 | console.log(res); 12 | if (res && res.length > 0) { 13 | return res[0]; 14 | } 15 | return res; 16 | } 17 | function getDay(date) { 18 | const date2 = new Date(); 19 | const date1 = new Date(date); 20 | const iDays = parseInt((Math.abs(date2.getTime() - date1.getTime()) / 1000 / 60 / 60 / 24), 10); 21 | return iDays; 22 | } 23 | 24 | function formatDate(date) { 25 | const tempDate = new Date(date); 26 | const year = tempDate.getFullYear(); 27 | const month = tempDate.getMonth() + 1; 28 | const day = tempDate.getDate(); 29 | let hour = tempDate.getHours(); 30 | let min = tempDate.getMinutes(); 31 | let second = tempDate.getSeconds(); 32 | const week = tempDate.getDay(); 33 | let str = ''; 34 | if (week === 0) { 35 | str = '星期日'; 36 | } else if (week === 1) { 37 | str = '星期一'; 38 | } else if (week === 2) { 39 | str = '星期二'; 40 | } else if (week === 3) { 41 | str = '星期三'; 42 | } else if (week === 4) { 43 | str = '星期四'; 44 | } else if (week === 5) { 45 | str = '星期五'; 46 | } else if (week === 6) { 47 | str = '星期六'; 48 | } 49 | if (hour < 10) { 50 | hour = `0${hour}`; 51 | } 52 | if (min < 10) { 53 | min = `0${min}`; 54 | } 55 | if (second < 10) { 56 | second = `0${second}`; 57 | } 58 | return `${year}-${month}-${day}日 ${hour}:${min} ${str}`; 59 | } 60 | /** 61 | * 延时函数 62 | * @param {*} ms 毫秒 63 | */ 64 | async function delay(ms) { 65 | return new Promise((resolve) => { setTimeout(() => { resolve(); }, ms); }); 66 | } 67 | function checkIsJDOrder(str) { 68 | const re = /^[0-9]{12}$/; 69 | return re.test(str); 70 | } 71 | function checkIsTBOrder(str) { 72 | const re = /^[0-9]{19}$/; 73 | return re.test(str); 74 | } 75 | function checkIsPDDOrder(str) { 76 | if (str && str.length > 6 && str[6] === '-') { 77 | const re = /^[0-9|-]{22}$/; 78 | return re.test(str); 79 | } 80 | return false; 81 | } 82 | function checkOrder(str) { 83 | console.log('--------', str); 84 | if (str) { 85 | // eslint-disable-next-line no-param-reassign 86 | str = str.trim(); 87 | if (checkIsJDOrder(str)) { 88 | return true; 89 | } 90 | if (checkIsTBOrder(str)) { 91 | return true; 92 | } 93 | if (checkIsPDDOrder(str)) { 94 | return true; 95 | } 96 | return false; 97 | } 98 | return false; 99 | } 100 | function getPlatform(url) { 101 | if (!url) { 102 | return ''; 103 | } 104 | if (url.indexOf('jd.com') != -1) { 105 | return 'jd'; 106 | } 107 | if (url.indexOf('tb.cn') != -1) { 108 | return 'tb'; 109 | } 110 | if (url.indexOf('yangkeduo.com') != -1) { 111 | return 'pdd'; 112 | } 113 | return ''; 114 | } 115 | function formatReplayStr(content) { 116 | let result = `------【粉丝福利购】------ 117 | 118 | 【商品名】:${content.goods_name || '你的宝贝'} 119 | 120 | 【电商平台】:${content.platform} 121 | 122 | 【优惠券】:${content.coupon || '无'} 123 | 124 | 【预计补贴】:${content.money || '无'} 125 | 126 | 【下单链接(或口令)】: 127 | ${content.url || '无'} 128 | 129 | 【注】: 130 | 1.通过打开以上链接下单 或 复制口令到对应平台app下单才能领补贴哦 131 | 132 | 2.下单完成后将订单号发送给我,用来绑定补贴订单 133 | 134 | 3.收货完成后,补贴将以微信🧧方式发送给您 135 | 136 | `; 137 | if (!content.coupon && !content.money) { 138 | result = ''; 139 | } 140 | return result; 141 | } 142 | 143 | const updateWechatFirend = function (contact) { 144 | // eslint-disable-next-line no-async-promise-executor 145 | return new Promise(async (resolve, reject) => { 146 | const contactName = contact.name(); 147 | const db_obj = await db_action.connectMongo('taobaoke'); 148 | const customers = await db_action.selectCustomers(db_obj, { wx_id: contact.id }); 149 | if (customers.length > 0) { 150 | // 客户已存在 151 | // 更新last_time字段 152 | const update_user_data = { 153 | new_name: contactName, 154 | last_time: moment().format('YYYY-MM-DD HH:mm:ss'), 155 | is_friend: contact.friend(), 156 | gender: contact.gender(), 157 | city: contact.city(), 158 | province: contact.province(), 159 | from: 'wechat', 160 | }; 161 | const phone = contact.phone() || contact.payload.phone; 162 | if (phone && Array.isArray(phone) && phone.length > 0) { 163 | update_user_data.new_phone = phone; 164 | } 165 | 166 | let new_alias = await contact.alias(); 167 | if (contact.payload.alias) { 168 | new_alias = contact.payload.alias; 169 | } 170 | if (new_alias) { 171 | update_user_data.new_alias = new_alias; 172 | } 173 | 174 | let avatar = await contact.avatar(); 175 | if (contact.payload.avatar) { 176 | avatar = contact.payload.avatar; 177 | } 178 | if (avatar) { 179 | update_user_data.avatar = avatar; 180 | } 181 | 182 | const type = contact.type() || contact.payload.type; 183 | if (type != '') { 184 | update_user_data.type = type; 185 | } 186 | await db_action.updateCustomer( 187 | db_obj, 188 | { 189 | wx_id: contact.id, 190 | }, 191 | update_user_data, 192 | ); 193 | } else { 194 | // 插入新客户 195 | await db_action.insertCustomer(db_obj, { 196 | wx_id: contact.id, 197 | name: contactName, 198 | phone: contact.phone() || contact.payload.phone, 199 | alias: contact.alias() || contact.payload.alias, 200 | avatar: contact.avatar() || contact.payload.avatar, 201 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 202 | is_friend: contact.friend(), 203 | gender: contact.gender(), 204 | city: contact.city(), 205 | province: contact.province(), 206 | type: contact.type() || contact.payload.type, 207 | from: 'wechat', 208 | }); 209 | } 210 | db_obj.conn.close(); 211 | resolve(); 212 | }); 213 | }; 214 | module.exports = { 215 | getHttpString, 216 | getDay, 217 | formatDate, 218 | delay, 219 | checkOrder, 220 | getPlatform, 221 | checkIsJDOrder, 222 | checkIsTBOrder, 223 | checkIsPDDOrder, 224 | formatReplayStr, 225 | updateWechatFirend, 226 | }; 227 | -------------------------------------------------------------------------------- /tbk-api-server/jd/index.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const { create } = require('./main'); 3 | const db_action = require('../../wechat/db_action'); 4 | const { setSchedule } = require('../../schedule'); 5 | const config = require('../../config'); 6 | const { delay } = require('../../utils'); 7 | 8 | const { appKey } = config.JDconfig; // 京东联盟分配给你的app_key 9 | const { appSecret } = config.JDconfig; // 京东联盟分配给你的app_secret 10 | 11 | const api = create( 12 | { 13 | appKey, 14 | appSecret, 15 | serverUrl: 'https://api.jd.com/routerjson', 16 | }, 17 | ); 18 | 19 | const getSkuId = (url) => { 20 | const res = url.match(/product\/(\S*)\./); 21 | if (res && res.length > 1) { 22 | return res[1]; 23 | } 24 | return ''; 25 | }; 26 | 27 | const getGoodsDetail = async (skuId) => api.request( 28 | 'jd.union.open.goods.promotiongoodsinfo.query', 29 | { 30 | skuIds: skuId, 31 | }, 32 | ).then((res) => { 33 | // eslint-disable-next-line max-len 34 | const result = JSON.parse(res.jd_union_open_goods_promotiongoodsinfo_query_responce.queryResult); 35 | if (result.data && Array.isArray(result.data)) { 36 | return result.data[0]; 37 | } 38 | return null; 39 | }); 40 | 41 | const getLink = async (url) => api.request( 42 | 'jd.union.open.promotion.common.get', 43 | { 44 | promotionCodeReq: { 45 | materialId: url, 46 | siteId: '4100464321', 47 | }, 48 | }, 49 | ).then((res) => { 50 | const result = JSON.parse(res.jd_union_open_promotion_common_get_responce.getResult); 51 | console.log('jd_result', result); 52 | if (result.data) { 53 | return result.data; 54 | } 55 | return null; 56 | }); 57 | 58 | const getOrderList = async (query = {}) => api.request( 59 | 'jd.union.open.order.row.query', 60 | { 61 | orderReq: { 62 | pageIndex: 1, 63 | pageSize: 500, 64 | type: 3, 65 | startTime: query.startTime || moment().subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss'), 66 | endTime: query.endTime || moment().subtract(5, 'seconds').format('YYYY-MM-DD HH:mm:ss'), 67 | }, 68 | }, 69 | ).then((res) => { 70 | // console.log(res) 71 | const result = JSON.parse(res.jd_union_open_order_row_query_responce.queryResult); 72 | console.log(result); 73 | if (result.data) { 74 | return result.data; 75 | } 76 | return null; 77 | }); 78 | 79 | const updateOrderDB = async function (orderList) { 80 | if (orderList && Array.isArray(orderList) && orderList.length > 0) { 81 | const db_obj = await db_action.connectMongo('taobaoke'); 82 | for (let i = 0; i < orderList.length; i += 1) { 83 | const item = orderList[i]; 84 | const orders = await db_action.selectOrders(db_obj, { order_id: item.orderId }); 85 | if (orders.length > 0) { 86 | await db_action.updateOrder(db_obj, { order_id: item.orderId }, { 87 | order_id: item.orderId, 88 | last_time: moment().format('YYYY-MM-DD HH:mm:ss'), 89 | platform: 'jd', 90 | parentId: item.parentId, 91 | order_detail: item, 92 | real: 1, 93 | goods_name: item.skuName, 94 | goods_num: item.skuNum, 95 | orderTime: item.orderTime, 96 | finishTime: item.finishTime, 97 | modifyTime: item.modifyTime, 98 | subsidyFee: item.actualFee * config.RAKE, 99 | estimateCosPrice: item.estimateCosPrice, // 预估计佣金额 100 | estimateFee: item.estimateFee, // 预估全部佣金金额 101 | estimateCustomerFee: item.estimateFee * config.RAKE, // 预估客户获得佣金金额 102 | }); 103 | } else { 104 | await db_action.insertOrder(db_obj, { 105 | order_id: item.orderId, 106 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 107 | platform: 'jd', 108 | parentId: item.parentId, 109 | order_detail: item, 110 | real: 1, 111 | goods_name: item.skuName, 112 | goods_num: item.skuNum, 113 | orderTime: item.orderTime, 114 | finishTime: item.finishTime, 115 | modifyTime: item.modifyTime, 116 | actualFee: item.actualFee, 117 | subsidyFee: item.actualFee * config.RAKE, 118 | estimateCosPrice: item.estimateCosPrice, // 预估计佣金额 119 | estimateFee: item.estimateFee, // 预估全部佣金金额 120 | estimateCustomerFee: item.estimateFee * config.RAKE, // 预估客户获得佣金金额 121 | 122 | }); 123 | } 124 | } 125 | db_obj.conn.close(); 126 | } 127 | }; 128 | const updateOrders = async () => { 129 | console.log('更新订单中...'); 130 | const orderList = await getOrderList(); 131 | await updateOrderDB(orderList); 132 | console.log('更新订单完毕...'); 133 | }; 134 | const updateOrdersEveryMinute = () => { 135 | setSchedule('0 * * * * *', updateOrders); 136 | }; 137 | async function updateOrderByDay(day = 1) { 138 | let result = []; 139 | for (let i = 24 * day; i > 0; i -= 1) { 140 | const startTime = moment().subtract(i, 'hours').format('YYYY-MM-DD HH:mm:ss'); 141 | const endTime = moment().subtract(i - 1, 'hours').format('YYYY-MM-DD HH:mm:ss'); 142 | const res = await getOrderList({ startTime, endTime }); 143 | await delay(500); 144 | console.log(startTime, endTime, res); 145 | if (res) { 146 | result = result.concat(res); 147 | } 148 | } 149 | await updateOrderDB(result); 150 | } 151 | const getPromotionReply = async (url) => { 152 | const skuId = getSkuId(url); 153 | let result = ''; 154 | const resultData = { 155 | platform: '京东', 156 | }; 157 | let goods_detail = null; 158 | if (skuId) { 159 | goods_detail = await getGoodsDetail(skuId); 160 | } else { 161 | return '🙁🙁🙁该宝贝无优惠,试试其他宝贝'; 162 | } 163 | if (!goods_detail) { 164 | return '🙁🙁🙁该宝贝无优惠,试试其他宝贝'; 165 | } 166 | resultData.goods_name = goods_detail.goodsName || ''; 167 | const linkInfo = await getLink(url); 168 | let price = goods_detail.unitPrice || 0; 169 | if (goods_detail.wlUnitPrice) { 170 | price = goods_detail.wlUnitPrice; 171 | } 172 | let rate = goods_detail.commisionRatioPc || 1; 173 | if (goods_detail.commisionRatioWl) { 174 | rate = goods_detail.commisionRatioWl; 175 | } 176 | const money = Number(price * (rate / 100) * config.RAKE).toFixed(2); 177 | if (money > 0) { 178 | resultData.money = money; 179 | result += `福利购 🧧🧧🧧 该商品有补贴${money}元 🧧🧧🧧`; 180 | } 181 | console.log('linkInfo', linkInfo); 182 | if (linkInfo.jCommand) { 183 | resultData.url = linkInfo.jCommand; 184 | result += `${linkInfo.jCommand}复制本条消息下单`; 185 | } else { 186 | resultData.url = linkInfo.clickURL; 187 | result += ` 下单点击:${linkInfo.clickURL}`; 188 | } 189 | result += "下单成功不要忘记发送订单号给我,领取补贴奖励哦"; 190 | return resultData; 191 | }; 192 | 193 | module.exports = { 194 | getPromotionReply, 195 | getOrderList, 196 | updateOrders, 197 | updateOrdersEveryMinute, 198 | updateOrderByDay, 199 | }; 200 | // getPromotionReply('https://item.m.jd.com/product/4718621.html?&utm_source=iosapp&utm_medium=appshare&utm_campaign=t_335139774&utm_term=CopyURL&ad_od=share&gx=RnFmlWdYb2GKmdRP--tzDnAdULrU3SjFqz2Z') 201 | -------------------------------------------------------------------------------- /tbk-api-server/pdd/index.js: -------------------------------------------------------------------------------- 1 | const PddSdk = require('pdd-sdk').default; 2 | const moment = require('moment'); 3 | const db_action = require('../../wechat/db_action'); 4 | const config = require('../../config'); 5 | // const { setSchedule } = require('../schedule') 6 | 7 | // 以调用商品标准类目接口为例 8 | // 商品标准类目接口文档(https://open.pinduoduo.com/application/document/api?id=pdd.goods.cats.get) 9 | const apiClient = new PddSdk({ 10 | clientId: config.PDDconfig.clientId, 11 | clientSecret: config.PDDconfig.clientSecret, 12 | }); 13 | 14 | /** 15 | * 从链接里提取参数值 16 | * @param {String} url 17 | * @param {String} key 18 | * @returns {String} 19 | */ 20 | const getUrlParamValByKey = (url, key) => { 21 | let clone_url = url; 22 | if (!clone_url) { 23 | return null; 24 | } 25 | clone_url = clone_url.replace(/&/g, "&"); 26 | const str = clone_url.split('?')[1]; 27 | const arr = str.split('&'); 28 | let res = ''; 29 | arr.forEach((element) => { 30 | const k = element.split('=')[0]; 31 | const v = element.split('=')[1]; 32 | if (key === k) { 33 | res = v; 34 | } 35 | }); 36 | return res; 37 | }; 38 | 39 | /** 40 | * 搜索商品 通过关键词 41 | * @param {*} keyword 42 | * @returns 43 | */ 44 | const searchGoods = function (keyword) { 45 | return apiClient.execute('pdd.ddk.goods.search', { pid: config.PDDconfig.pid, keyword, page_size: 100 }).then((res) => { 46 | let goods_list = []; 47 | if (res.goods_search_response && res.goods_search_response.goods_list) { 48 | goods_list = res.goods_search_response.goods_list; 49 | } 50 | return goods_list; 51 | }).catch((error) => { 52 | console.log(error); 53 | }); 54 | }; 55 | 56 | /** 57 | * 多多进宝推广链接生成,通过goods_sign 58 | * @param {*} goods_sign 59 | * @returns 60 | */ 61 | const generateUrl = function (goods_sign) { 62 | return apiClient.execute('pdd.ddk.goods.promotion.url.generate', { p_id: config.PDDconfig.pid, goods_sign_list: [goods_sign] }).then((res) => { 63 | let goods_list = []; 64 | // eslint-disable-next-line max-len 65 | if (res.goods_promotion_url_generate_response && res.goods_promotion_url_generate_response.goods_promotion_url_list) { 66 | goods_list = res.goods_promotion_url_generate_response.goods_promotion_url_list; 67 | } 68 | if (goods_list.length > 0) { 69 | return goods_list[0].short_url; 70 | } 71 | return null; 72 | }).catch((error) => { 73 | console.log(error); 74 | }); 75 | }; 76 | 77 | // 转链 78 | const getLink = function (source_url) { 79 | return apiClient.execute('pdd.ddk.goods.zs.unit.url.gen', { pid: config.PDDconfig.pid, source_url }).then((res) => res.goods_zs_unit_generate_response).catch((error) => { 80 | console.log(error); 81 | }); 82 | }; 83 | 84 | // 获取商品详情 85 | const getGoodsDetail = function (goods_sign) { 86 | return apiClient.execute('pdd.ddk.goods.detail', { pid: config.PDDconfig.pid, goods_sign }).then((res) => { 87 | if (res.goods_detail_response && res.goods_detail_response.goods_details) { 88 | return res.goods_detail_response.goods_details[0]; 89 | } 90 | return null; 91 | }).catch((error) => { 92 | console.log(error); 93 | }); 94 | }; 95 | 96 | const getPromoteLink = async function (source_url) { 97 | const links = await getLink(source_url); 98 | if (!links) { 99 | return Promise.resolve('该宝贝暂无优惠'); 100 | } 101 | const goods_sign = getUrlParamValByKey(links.url, 'goods_sign'); 102 | if (!goods_sign) { 103 | return Promise.resolve('该宝贝暂无优惠'); 104 | } 105 | const short_url = await generateUrl(goods_sign); 106 | const goods_detail = await getGoodsDetail(goods_sign); 107 | // const goods_name = goods_detail.goods_name || ''; 108 | // const search_goods_list = await searchGoods(goods_name); 109 | return new Promise((resolve, reject) => { 110 | const resultData = { 111 | platform: '拼多多', 112 | }; 113 | if (!goods_detail) { 114 | resolve('该宝贝暂无优惠'); 115 | return; 116 | } 117 | resultData.goods_name = goods_detail.goods_name || ''; 118 | const promotion_rate = goods_detail.promotion_rate > 0 119 | ? goods_detail.promotion_rate / 1000 120 | : 0; 121 | const goods_price = goods_detail.min_group_price / 100; 122 | const money = parseFloat(goods_price * promotion_rate).toFixed(2); 123 | 124 | let result = `您查询的拼多多宝贝:${goods_detail.goods_name} `; 125 | if (goods_detail.extra_coupon_amount > 0) { 126 | resultData.coupon = `${goods_detail.extra_coupon_amount / 100}元`; 127 | result += `有内部优惠券:${goods_detail.extra_coupon_amount / 100}元 `; 128 | } 129 | if (money > 0) { 130 | resultData.money = money || ''; 131 | result += `预计补贴:${money}元 🧧🧧🧧 `; 132 | } 133 | resultData.url = short_url || links.short_url; 134 | result += `点击下单(有补贴):${resultData.url} 下单完成请将订单号发送给我,绑定补贴哦!`; 135 | // console.log(result); 136 | resolve(resultData); 137 | }); 138 | }; 139 | 140 | // 获取订单详情 141 | const getOrderDetail = function (order_sn) { 142 | return apiClient.execute('pdd.ddk.order.detail.get', { order_sn }).then((res) => { 143 | console.log(res.order_detail_response); 144 | if (res.order_detail_response) { 145 | return res.order_detail_response; 146 | } 147 | return null; 148 | }).catch((error) => { 149 | console.log(error); 150 | }); 151 | }; 152 | // 获取订单列表 153 | const getOrderList = function () { 154 | return apiClient.execute('pdd.ddk.order.list.range.get', { 155 | start_time: moment().subtract(30, 'days').format('YYYY-MM-DD HH:mm:ss'), 156 | end_time: moment().subtract(5, 'seconds').format('YYYY-MM-DD HH:mm:ss'), 157 | }).then((res) => { 158 | console.log(res.order_list_get_response.order_list); 159 | if (res.order_list_get_response.order_list) { 160 | return res.order_list_get_response.order_list; 161 | } 162 | return null; 163 | }).catch((error) => { 164 | console.log(error); 165 | }); 166 | }; 167 | const updateOrders = async () => { 168 | console.log('更新订单中...'); 169 | const orderList = await getOrderList(); 170 | if (orderList && Array.isArray(orderList) && orderList.length > 0) { 171 | const db_obj = await db_action.connectMongo('taobaoke'); 172 | for (let i = 0; i < orderList.length; i += 1) { 173 | const item = orderList[i]; 174 | const orders = await db_action.selectOrders(db_obj, { order_id: item.order_sn }); 175 | if (orders.length > 0) { 176 | await db_action.updateOrder(db_obj, { order_id: item.order_sn }, { 177 | order_id: item.order_sn, 178 | last_time: moment().format('YYYY-MM-DD HH:mm:ss'), 179 | platform: 'pdd', 180 | order_detail: item, 181 | real: 1, 182 | orderTime: item.order_create_time ? moment(item.order_create_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 183 | finishTime: item.order_receive_time ? moment(item.order_receive_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 184 | modifyTime: item.order_modify_at ? moment(item.order_modify_at * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 185 | actualFee: Number(item.promotion_amount / 100).toFixed(2) || '', 186 | subsidyFee: Number((item.promotion_amount / 100) * config.RAKE).toFixed(2) || '', 187 | }); 188 | } else { 189 | await db_action.insertOrder(db_obj, { 190 | order_id: item.order_sn, 191 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 192 | platform: 'pdd', 193 | order_detail: item, 194 | real: 1, 195 | orderTime: item.order_create_time ? moment(item.order_create_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 196 | orderPayTime: item.order_pay_time ? moment(item.order_pay_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 197 | finishTime: item.order_receive_time ? moment(item.order_receive_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 198 | settleTime: item.order_settle_time ? moment(item.order_settle_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 199 | modifyTime: item.order_modify_at ? moment(item.order_modify_at * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 200 | actualFee: Number(item.promotion_amount / 100).toFixed(2) || '', 201 | subsidyFee: Number((item.promotion_amount / 100) * config.RAKE).toFixed(2) || '', 202 | }); 203 | } 204 | } 205 | db_obj.conn.close(); 206 | } 207 | console.log('更新订单完毕...'); 208 | }; 209 | // const updateOrdersEveryMinute = () => { 210 | // setSchedule('0 * * * * *', updateOrders) 211 | // } 212 | 213 | // let url = 'https://mobile.yangkeduo.com/goods2.html?_wvx=10&refer_share_uin=AKDNTPXMYINS5JHETQKJVRGXXM_GEXDA&refer_share_uid=8111396033479&share_uin=AKDNTPXMYINS5JHETQKJVRGXXM_GEXDA&page_from=205&_wv=41729&refer_share_id=Lr3nfeaNxcPdL0pcKcoKwkpBJzD7q090&refer_share_channel=copy_link&share_uid=8111396033479&pxq_secret_key=IK45E6VZ3JDTOZKAD6PP2WJAGVOZ6JBDUUY6CBTE2VKUMRFSQ54A&goods_id=261568776516' 214 | // getPromoteLink(url) 215 | // getOrderDetail('211221-345820404612637') 216 | module.exports = { 217 | getPromoteLink, 218 | getOrderDetail, 219 | updateOrders, 220 | // updateOrdersEveryMinute 221 | }; 222 | -------------------------------------------------------------------------------- /wechat/service/msg-filters.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const moment = require('moment'); 3 | const config = require('../../config'); 4 | const db_action = require('../db_action'); 5 | const pdd = require('../../tbk-api-server/pdd'); 6 | const jd = require('../../tbk-api-server/jd'); 7 | const utils = require('../../utils'); 8 | const msgs = require('../msg'); 9 | 10 | function emptyMsg() { 11 | const msgArr = []; // 返回的消息列表 12 | const obj = { type: 1, content: '我在呢', url: '' }; // 消息主体 13 | msgArr.push(obj); 14 | return msgArr; 15 | } 16 | 17 | function officialMsg() { 18 | console.log('无效或官方消息,不做回复'); 19 | return [{ type: 1, content: '', url: '' }]; 20 | } 21 | 22 | function newFriendMsg({ name }) { 23 | console.log(`新添加好友:${name},默认回复`); 24 | return config.newFriendReplys || [{ type: 1, content: msgs.subscribeMsg, url: '' }]; 25 | } 26 | 27 | function searchPromoteLink({ 28 | that, msg, id, contact, name, 29 | }) { 30 | // 查转链链接 31 | return axios({ 32 | url: 'http://127.0.0.1:3333', 33 | method: 'GET', 34 | params: { url: msg }, 35 | }).then(async (res) => { 36 | console.log('转链结果', res.data); 37 | if (!res.data) { 38 | return [{ type: 1, content: '☹该商品暂无优惠和补贴,换一个试试', url: '' }]; 39 | } 40 | const db_obj = await db_action.connectMongo('taobaoke'); 41 | const customers = await db_action.selectCustomers(db_obj, { wx_id: id }); 42 | const customer = customers[0]; 43 | console.log('转链人:', customer, id); 44 | db_action.insertTurnLink(db_obj, { 45 | origin_url: msg, 46 | platform: utils.getPlatform(msg), 47 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 48 | turn_content: res.data, 49 | customer_id: customer._id.toString(), 50 | customer_name: customer.name || '', 51 | from: 'wechat', 52 | }).finally(() => { 53 | db_obj.conn.close(); 54 | }); 55 | return [ 56 | { type: 1, content: res.data, url: '' }, 57 | { type: 1, content: `下单完成后将【订单号】发送给我!`, url: '' }, 58 | ]; 59 | }).catch((err) => { 60 | console.log('err', err); 61 | }); 62 | } 63 | 64 | async function bindOrder({ messages, id }) { 65 | // msg用户发的订单号 66 | let msg = messages; 67 | if (msg) { 68 | msg = String(msg).trim(); 69 | } 70 | let replay_str = '请检查订单号'; 71 | const db_obj = await db_action.connectMongo('taobaoke'); 72 | const customers = await db_action.selectCustomers(db_obj, { wx_id: id }); 73 | const customer = customers[0]; 74 | console.log('绑定订单人:', customer, id); 75 | if (utils.checkIsJDOrder(msg)) { 76 | const orders = await db_action.selectOrders(db_obj, { order_id: msg }); 77 | if (orders.length > 0 && orders[0].is_bind) { 78 | replay_str = '该京东补贴订单已被绑定,不要重复绑定!'; 79 | } else { 80 | const real_orders = await jd.getOrderList(); 81 | let real_order = null; 82 | if (real_orders && real_orders.length > 0) { 83 | real_orders.forEach((item) => { 84 | if (item.orderId == msg) { 85 | real_order = item; 86 | } else if (item.parentId == msg) { 87 | real_order = item; 88 | } 89 | }); 90 | } 91 | if (real_order) { 92 | await db_action.insertOrder(db_obj, { 93 | order_id: msg, 94 | is_bind: 1, 95 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 96 | customer_id: customer._id.toString(), 97 | customer_name: customer.name || '', 98 | platform: 'jd', 99 | parentId: real_order ? real_order.parentId : null, 100 | order_detail: real_order, 101 | real: real_order ? 1 : 0, 102 | goods_name: real_order.skuName, 103 | goods_num: real_order.skuNum, 104 | orderTime: real_order.orderTime || null, 105 | finishTime: real_order.finishTime || null, 106 | modifyTime: real_order.modifyTime || null, 107 | actualFee: real_order.actualFee, 108 | subsidyFee: real_order.actualFee * config.RAKE, 109 | estimateCosPrice: real_order.estimateCosPrice, // 预估计佣金额 110 | estimateFee: real_order.estimateFee, // 预估全部佣金金额 111 | estimateCustomerFee: real_order.estimateFee * config.RAKE, // 预估客户获得佣金金额 112 | from: 'wechat', 113 | }); 114 | replay_str = '您的京东补贴订单绑定成功!'; 115 | if (real_order.estimateCosPrice) { 116 | replay_str += `预计本单补贴:${real_order.estimateFee * config.RAKE}元,收货后(5-15天)将以微信🧧方式发送给您,请注意查收!`; 117 | } 118 | } else { 119 | await db_action.insertOrder(db_obj, { 120 | order_id: msg, 121 | is_bind: 1, 122 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 123 | customer_id: customer._id.toString(), 124 | customer_name: customer.name || '', 125 | real: real_order ? 1 : 0, 126 | platform: 'jd', 127 | from: 'wechat', 128 | }); 129 | replay_str = '您的京东补贴订单绑定成功!收货后(5-15天)将以微信🧧方式发送给您,请注意查收!'; 130 | } 131 | } 132 | } 133 | // 淘宝订单号 134 | if (utils.checkIsTBOrder(msg)) { 135 | const orders = await db_action.selectOrders(db_obj, { order_id: msg }); 136 | if (orders.length > 0) { 137 | replay_str = '该淘宝补贴订单已被绑定,不要重复绑定!'; 138 | } else { 139 | await db_action.insertOrder(db_obj, { 140 | order_id: msg, 141 | is_bind: 1, 142 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 143 | customer_id: customer._id.toString(), 144 | customer_name: customer.name || '', 145 | platform: 'tb', 146 | from: 'wechat', 147 | }); 148 | replay_str = '您的淘宝补贴订单绑定成功!收货后(5-15天)将以微信🧧方式发送给您,请注意查收!'; 149 | } 150 | } 151 | // 拼多多订单号 152 | if (utils.checkIsPDDOrder(msg)) { 153 | const orders = await db_action.selectOrders(db_obj, { order_id: msg }); 154 | if (orders.length > 0 && orders[0].is_bind) { 155 | replay_str = '该拼多多补贴订单已被绑定,不要重复绑定!'; 156 | } else { 157 | const order_detail = await pdd.getOrderDetail(msg); 158 | if (order_detail) { 159 | await db_action.insertOrder(db_obj, { 160 | order_id: msg, 161 | is_bind: 1, 162 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 163 | customer_id: customer._id.toString(), 164 | customer_name: customer.name || '', 165 | platform: 'pdd', 166 | real: order_detail ? 1 : 0, 167 | order_detail, 168 | orderTime: order_detail.order_create_time ? moment(order_detail.order_create_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 169 | orderPayTime: order_detail.order_pay_time ? moment(order_detail.order_pay_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 170 | finishTime: order_detail.order_receive_time ? moment(order_detail.order_receive_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 171 | settleTime: order_detail.order_settle_time ? moment(order_detail.order_settle_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 172 | modifyTime: order_detail.order_modify_at ? moment(order_detail.order_modify_at * 1000).format('YYYY-MM-DD HH:mm:ss') : '', 173 | actualFee: Number(order_detail.promotion_amount / 100).toFixed(2) || '', 174 | subsidyFee: Number((order_detail.promotion_amount / 100) * config.RAKE).toFixed(2) || '', 175 | from: 'wechat', 176 | }); 177 | replay_str = '您的拼多多补贴订单绑定成功!'; 178 | const subsidyFee = Number((order_detail.promotion_amount / 100) * config.RAKE) 179 | .toFixed(2); 180 | if (subsidyFee) { 181 | replay_str += `预计本单补贴:${subsidyFee}元,收货后(5-15天)将以微信🧧方式发送给您,请注意查收!`; 182 | } 183 | } else { 184 | await db_action.insertOrder(db_obj, { 185 | order_id: msg, 186 | is_bind: 1, 187 | create_time: moment().format('YYYY-MM-DD HH:mm:ss'), 188 | customer_id: customer._id.toString(), 189 | customer_name: customer.name || '', 190 | platform: 'pdd', 191 | real: order_detail ? 1 : 0, 192 | order_detail: null, 193 | from: 'wechat', 194 | }); 195 | replay_str = '您的拼多多补贴订单绑定成功!收货后(5-15天)将以微信🧧方式发送给您,请注意查收!'; 196 | } 197 | } 198 | } 199 | db_obj.conn.close(); 200 | return [{ type: 1, content: replay_str, url: '' }]; 201 | } 202 | /** 203 | * 关键词回复 204 | * @returns {Promise<*>} 205 | */ 206 | 207 | async function keywordsMsg({ msg }) { 208 | let res = []; 209 | try { 210 | if (config.replyKeywords && config.replyKeywords.length > 0) { 211 | for (let i = 0; i < Object.keys(config.replyKeywords).length; i += 1) { 212 | const item = config.replyKeywords[i]; 213 | if (item.reg === 2 && item.keywords.includes(msg)) { 214 | console.log(`精确匹配到关键词${msg},正在回复用户`); 215 | res = item.replys; 216 | } 217 | if (item.reg === 1) { 218 | for (let j = 0; j < Object.keys(item.keywords).length; j += 1) { 219 | const key = item.keywords[j]; 220 | if (msg.includes(key)) { 221 | console.log(`模糊匹配到关键词${msg},正在回复用户`); 222 | res = item.replys; 223 | } 224 | } 225 | } 226 | } 227 | } 228 | return res; 229 | } catch (e) { 230 | console.log('keywordsMsg error:', e); 231 | return []; 232 | } 233 | } 234 | 235 | module.exports = { 236 | emptyMsg, 237 | officialMsg, 238 | newFriendMsg, 239 | keywordsMsg, 240 | searchPromoteLink, 241 | bindOrder, 242 | }; 243 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/main.js: -------------------------------------------------------------------------------- 1 | const TopClient = require("./lib/api/topClient"); 2 | const config = require("../../config"); 3 | 4 | const client = new TopClient.TopClient({ 5 | appkey: config.TBconfig.appkey, 6 | appsecret: config.TBconfig.appsecret, 7 | REST_URL: "http://gw.api.taobao.com/router/rest", 8 | }); 9 | 10 | /** 11 | * 获取淘口令 12 | * @param {String} url 商品推广链接 13 | * @returns {String} 淘口令 14 | */ 15 | const getTKL = (url) => { 16 | console.log("查询tkl:", url); 17 | return new Promise((resolve, reject) => { 18 | client.execute( 19 | "taobao.tbk.tpwd.create", 20 | { 21 | url, 22 | // 'text':'数据结构JSON示例', 23 | // 'logo':'数据结构JSON示例', 24 | // 'ext':'数据结构JSON示例', 25 | // 'user_id':'数据结构JSON示例' 26 | }, 27 | (error, response) => { 28 | console.log("查询tkl_res", response); 29 | if (!error) { 30 | let res = ""; 31 | if (response && response.data) { 32 | if (response.data.model) { 33 | res = response.data.model; 34 | } 35 | if (response.data.password_simple) { 36 | res = response.data.password_simple; 37 | } 38 | } else { 39 | res = null; 40 | } 41 | resolve(res); 42 | } else { 43 | resolve(null); 44 | } 45 | }, 46 | ); 47 | }); 48 | }; 49 | 50 | const getGoodsId = (click_url) => new Promise((resolve, reject) => { 51 | client.execute( 52 | "taobao.tbk.item.click.extract", 53 | { 54 | click_url, 55 | // 'platform':'1', 56 | // 'ip':'11.22.33.43' 57 | }, 58 | (error, response) => { 59 | if (!error) { 60 | // console.log("extract====>", response); 61 | resolve(); 62 | // eslint-disable-next-line max-len 63 | // if (response.results && Array.isArray(response.results.n_tbk_item) && response.results.n_tbk_item.length > 0) { 64 | // resolve(response.results.n_tbk_item[0]); 65 | // } else { 66 | // resolve(null); 67 | // } 68 | } else { 69 | reject(error); 70 | } 71 | }, 72 | ); 73 | }); 74 | 75 | /** 76 | * 淘宝客商品详情查询(简版) 77 | * @param {String} id 商品id 78 | * @returns {Object} 79 | */ 80 | const getGoodsDetail = (id) => new Promise((resolve, reject) => { 81 | client.execute( 82 | "taobao.tbk.item.info.get", 83 | { 84 | num_iids: id, 85 | // 'platform':'1', 86 | // 'ip':'11.22.33.43' 87 | }, 88 | (error, response) => { 89 | if (!error) { 90 | // console.log("====>", response); 91 | // eslint-disable-next-line max-len 92 | if ( 93 | response.results 94 | && Array.isArray(response.results.n_tbk_item) 95 | && response.results.n_tbk_item.length > 0 96 | ) { 97 | resolve(response.results.n_tbk_item[0]); 98 | } else { 99 | resolve(null); 100 | } 101 | } else { 102 | reject(error); 103 | } 104 | }, 105 | ); 106 | }); 107 | 108 | /** 109 | * 通过商品id对比得到转链商品信息 110 | * @param {String} goods_id 商品id 111 | * @param {Array} goods_list 商品列表 112 | * @returns {String} 113 | */ 114 | const getPromotionInfo = (goods_id, goods_list) => { 115 | let result = ""; 116 | for (let i = 0; i < goods_list.length; i += 1) { 117 | if (goods_list[i].item_id == goods_id) { 118 | result = goods_list[i]; 119 | break; 120 | } 121 | } 122 | return result; 123 | }; 124 | 125 | /** 126 | * 通过关键词搜索商品 127 | * @param {String} keyword 关键词 128 | * @param {String} seller_ids 商家ids 129 | * @param {Object} search_goods_detail 查询的商品详情信息 130 | * @param {String} page_result_key 本地化业务入参-分页唯一标识,非首页的请求必传,值为上一页返回结果中的page_result_key字段值 131 | * @returns {Array} 132 | */ 133 | const getGoodsListByKeyWord = (keyword, page = 1, page_result_key = "") => 134 | // console.log("keywordkeywordkeywordkeyword", keyword); 135 | new Promise((resolve, reject) => { 136 | client.execute( 137 | "taobao.tbk.dg.material.optional", 138 | { 139 | // 'start_dsr': '10', 140 | page_size: "100", 141 | page_no: page, 142 | // 'platform': '1', 143 | // 'end_tk_rate': '500', 144 | // 'start_tk_rate': '6800', 145 | // 'end_price':'10', 146 | // 'start_price':'10', 147 | // 'is_overseas':'false', 148 | // 'is_tmall':'false', 149 | sort: "match", 150 | // 'itemloc':search_goods_detail.provcity.split(' ')[1]||'', 151 | // 'cat':'16,18', 152 | q: keyword, 153 | // 'material_id': '17004', 154 | // 'has_coupon': 'true', 155 | // 'ip':'13.2.33.4', 156 | adzone_id: config.TBconfig.adzone_id, 157 | // 'need_free_shipment':'true', 158 | // 'need_prepay': 'true', 159 | // 'include_pay_rate_30': 'true', 160 | // 'include_good_rate': 'true', 161 | // 'include_rfd_rate': 'true', 162 | // 'npx_level': '2', 163 | // 'end_ka_tk_rate': '0', 164 | // 'start_ka_tk_rate': '0', 165 | // 'device_encrypt': 'MD5', 166 | // 'device_value':'xxx', 167 | // 'device_type': 'IMEI', 168 | // 'lock_rate_end_time':'1567440000000', 169 | // 'lock_rate_start_time':'1567440000000', 170 | // 'longitude':'121.473701', 171 | // 'latitude':'31.230370', 172 | // 'city_code':'310000', 173 | // 'seller_ids':search_goods_detail.seller_id||'', 174 | // 'special_id':'2323', 175 | // 'relation_id':'3243', 176 | // 'page_result_key': page_result_key, 177 | // 'ucrowd_id':'1', 178 | // 'ucrowd_rank_items':'数据结构JSON示例', 179 | get_topn_rate: "0", 180 | }, 181 | (error, response) => { 182 | if (!error) { 183 | const result_list = response.result_list || null; 184 | const map_data = result_list.map_data || null; 185 | if (Array.isArray(map_data) && map_data.length > 0) { 186 | // console.log(response.total_results); 187 | // console.log(map_data); 188 | resolve({ 189 | total_results: response.total_results, 190 | map_data, 191 | }); 192 | } else { 193 | resolve(null); 194 | } 195 | } else { 196 | reject(error); 197 | } 198 | }, 199 | ); 200 | }); 201 | 202 | /** 203 | * 从链接里提取参数值 204 | * @param {String} url 205 | * @param {String} key 206 | * @returns {String} 207 | */ 208 | const getUrlParamValByKey = (url, key) => { 209 | let clone_url = url; 210 | if (!clone_url) { 211 | return null; 212 | } 213 | clone_url = clone_url.replace(/&/g, "&"); 214 | const str = clone_url.split("?")[1]; 215 | const arr = str.split("&"); 216 | let res = ""; 217 | arr.forEach((element) => { 218 | const k = element.split("=")[0]; 219 | const v = element.split("=")[1]; 220 | if (key == k) { 221 | res = v; 222 | } 223 | }); 224 | return res; 225 | }; 226 | 227 | const httpString = (s) => { 228 | let res = s; 229 | const reg = /(https?|http|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g; 230 | res = res.match(reg); 231 | if (res && res.length > 0) { 232 | return res[0]; 233 | } 234 | return res; 235 | }; 236 | const getLastPromotionInfo = async (into_goods_url) => { 237 | let keyword = ''; 238 | try { 239 | keyword = into_goods_url.match(/「([\S|\s]*)」/)[1]; 240 | } catch (error) { 241 | return null; 242 | } 243 | let result_item = null; 244 | let is_err = false; 245 | const jsonData = await getGoodsListByKeyWord(keyword).catch((err) => { 246 | console.log("=====", err); 247 | is_err = true; 248 | }); 249 | if (is_err) { 250 | return null; 251 | } 252 | const my_goods_list = jsonData.map_data; 253 | const total = jsonData.total_results; 254 | console.log("总数", total); 255 | console.log(my_goods_list) 256 | if (Array.isArray(my_goods_list) && my_goods_list.length > 0) { 257 | my_goods_list.some((item) => { 258 | if (item.title === keyword) { 259 | result_item = my_goods_list[0]; 260 | return true; 261 | } 262 | }); 263 | } 264 | if (!result_item) { 265 | let pages = 1; 266 | if (total > 100) { 267 | pages = Math.ceil(total / 100); 268 | } 269 | if (pages > 10) { 270 | pages = 10; 271 | } 272 | if (pages > 1) { 273 | for (let i = 2; i <= pages; i += 1) { 274 | let is_continue = false; 275 | const res = await getGoodsListByKeyWord(keyword, i).catch((err) => { 276 | is_continue = true; 277 | }); 278 | if (!is_continue && res && res.map_data && res.map_data.length > 0) { 279 | res.map_data.some((item) => { 280 | if (item.title === keyword) { 281 | result_item = my_goods_list[0]; 282 | return true; 283 | } 284 | }); 285 | if (result_item) { 286 | break; 287 | } 288 | } 289 | } 290 | } 291 | } 292 | if (!result_item) { 293 | return null; 294 | } 295 | let goods_url = "https:"; 296 | if (result_item.coupon_share_url) { 297 | goods_url += result_item.coupon_share_url; 298 | } else if (result_item.url) { 299 | goods_url += result_item.url; 300 | } else { 301 | return null; 302 | } 303 | const tkl_info = await getTKL(goods_url); 304 | result_item.tkl_info = tkl_info || null; 305 | return result_item; 306 | }; 307 | 308 | module.exports = getLastPromotionInfo; 309 | -------------------------------------------------------------------------------- /tbk-api-server/taobao/lib/api/network.js: -------------------------------------------------------------------------------- 1 | var StringDecoder = require('string_decoder').StringDecoder 2 | var FormData = require('form-data') 3 | var Stream = require('stream') 4 | var mime = require('mime') 5 | var path = require('path') 6 | var URL = require('url') 7 | var fs = require('fs') 8 | 9 | /** 10 | * Define form mime type 11 | */ 12 | mime.define({ 13 | 'application/x-www-form-urlencoded': ['form', 'urlencoded', 'form-data'] 14 | }) 15 | 16 | /** 17 | * Initialize our Rest Container 18 | */ 19 | var RestClient = function (method, uri, headers, body, callback) { 20 | var restClient = function (uri, headers, body, callback) { 21 | var $this = { 22 | /** 23 | * Stream Multipart form-data request 24 | * 25 | * @type {Boolean} 26 | */ 27 | _stream: false, 28 | 29 | /** 30 | * Container to hold multipart form data for processing upon request. 31 | * 32 | * @type {Array} 33 | * @private 34 | */ 35 | _multipart: [], 36 | 37 | /** 38 | * Container to hold form data for processing upon request. 39 | * 40 | * @type {Array} 41 | * @private 42 | */ 43 | _form: [], 44 | 45 | /** 46 | * Request option container for details about the request. 47 | * 48 | * @type {Object} 49 | */ 50 | options: { 51 | /** 52 | * Url obtained from request method arguments. 53 | * 54 | * @type {String} 55 | */ 56 | url: uri, 57 | 58 | /** 59 | * Method obtained from request method arguments. 60 | * 61 | * @type {String} 62 | */ 63 | method: method, 64 | 65 | /** 66 | * List of headers with case-sensitive fields. 67 | * 68 | * @type {Object} 69 | */ 70 | headers: {} 71 | }, 72 | 73 | hasHeader: function (name) { 74 | var headers 75 | var lowercaseHeaders 76 | 77 | name = name.toLowerCase() 78 | headers = Object.keys($this.options.headers) 79 | lowercaseHeaders = headers.map(function (header) { 80 | return header.toLowerCase() 81 | }) 82 | 83 | for (var i = 0; i < lowercaseHeaders.length; i++) { 84 | if (lowercaseHeaders[i] === name) { 85 | return headers[i] 86 | } 87 | } 88 | 89 | return false 90 | }, 91 | 92 | field: function (name, value, options) { 93 | return handleField(name, value, options) 94 | }, 95 | 96 | attach: function (name, path, options) { 97 | options = options || {} 98 | options.attachment = true 99 | return handleField(name, path, options) 100 | }, 101 | 102 | rawField: function (name, value, options) { 103 | $this._multipart.push({ 104 | name: name, 105 | value: value, 106 | options: options, 107 | attachment: options.attachment || false 108 | }) 109 | }, 110 | 111 | header: function (field, value) { 112 | if (is(field).a(Object)) { 113 | for (var key in field) { 114 | if (field.hasOwnProperty(key)) { 115 | $this.header(key, field[key]) 116 | } 117 | } 118 | 119 | return $this 120 | } 121 | 122 | var existingHeaderName = $this.hasHeader(field) 123 | $this.options.headers[existingHeaderName || field] = value 124 | 125 | return $this 126 | }, 127 | 128 | type: function (type) { 129 | $this.header('Content-Type', does(type).contain('/') 130 | ? type 131 | : mime.lookup(type)) 132 | return $this 133 | }, 134 | 135 | send: function (data) { 136 | var type = $this.options.headers[$this.hasHeader('content-type')] 137 | 138 | if ((is(data).a(Object) || is(data).a(Array)) && !Buffer.isBuffer(data)) { 139 | if (!type) { 140 | $this.type('form') 141 | type = $this.options.headers[$this.hasHeader('content-type')] 142 | $this.options.body = RestClient.serializers.form(data) 143 | } else if (~type.indexOf('json')) { 144 | $this.options.json = true 145 | 146 | if ($this.options.body && is($this.options.body).a(Object)) { 147 | for (var key in data) { 148 | if (data.hasOwnProperty(key)) { 149 | $this.options.body[key] = data[key] 150 | } 151 | } 152 | } else { 153 | $this.options.body = data 154 | } 155 | } else { 156 | $this.options.body = RestClient.Request.serialize(data, type) 157 | } 158 | } else if (is(data).a(String)) { 159 | if (!type) { 160 | $this.type('form') 161 | type = $this.options.headers[$this.hasHeader('content-type')] 162 | } 163 | 164 | if (type === 'application/x-www-form-urlencoded') { 165 | $this.options.body = $this.options.body 166 | ? $this.options.body + '&' + data 167 | : data 168 | } else { 169 | $this.options.body = ($this.options.body || '') + data 170 | } 171 | } else { 172 | $this.options.body = data 173 | } 174 | 175 | return $this 176 | }, 177 | 178 | end: function (callback) { 179 | var Request 180 | var header 181 | var parts 182 | var form 183 | 184 | function handleRequestResponse (error, response, body) { 185 | var result = {} 186 | // Handle pure error 187 | if (error && !response) { 188 | result.error = error 189 | 190 | if (callback) { 191 | callback(result) 192 | } 193 | 194 | return 195 | } 196 | 197 | if (!response) { 198 | console.log('This is odd, report this action / request to: http://github.com/mashape/RestClient-nodejs') 199 | 200 | result.error = { 201 | message: 'No response found.' 202 | } 203 | 204 | if (callback) { 205 | callback(result) 206 | } 207 | 208 | return 209 | } 210 | 211 | // Create response reference 212 | result = response 213 | 214 | body = body || response.body 215 | result.raw_body = body 216 | result.headers = response.headers 217 | 218 | if (body) { 219 | type = RestClient.type(result.headers['content-type'], true) 220 | if (type) data = RestClient.Response.parse(body, type) 221 | else data = body 222 | } 223 | result.body = data 224 | 225 | ;(callback) && callback(result) 226 | } 227 | 228 | function handleGZIPResponse (response) { 229 | if (/^(deflate|gzip)$/.test(response.headers['content-encoding'])) { 230 | var unzip = zlib.createUnzip() 231 | var stream = new Stream() 232 | var _on = response.on 233 | var decoder 234 | 235 | // Keeping node happy 236 | stream.req = response.req 237 | 238 | // Make sure we emit prior to processing 239 | unzip.on('error', function (error) { 240 | // Catch the parser error when there is no content 241 | if (error.errno === zlib.Z_BUF_ERROR || error.errno === zlib.Z_DATA_ERROR) { 242 | stream.emit('end') 243 | return 244 | } 245 | 246 | stream.emit('error', error) 247 | }) 248 | 249 | // Start the processing 250 | response.pipe(unzip) 251 | 252 | // Ensure encoding is captured 253 | response.setEncoding = function (type) { 254 | decoder = new StringDecoder(type) 255 | } 256 | 257 | // Capture decompression and decode with captured encoding 258 | unzip.on('data', function (buffer) { 259 | if (!decoder) return stream.emit('data', buffer) 260 | var string = decoder.write(buffer) 261 | if (string.length) stream.emit('data', string) 262 | }) 263 | 264 | // Emit yoself 265 | unzip.on('end', function () { 266 | stream.emit('end') 267 | }) 268 | 269 | response.on = function (type, next) { 270 | if (type === 'data' || type === 'end') { 271 | stream.on(type, next) 272 | } else if (type === 'error') { 273 | _on.call(response, type, next) 274 | } else { 275 | _on.call(response, type, next) 276 | } 277 | } 278 | } 279 | } 280 | 281 | function handleFormData (form) { 282 | for (var i = 0; i < $this._multipart.length; i++) { 283 | var item = $this._multipart[i] 284 | 285 | if (item.attachment && is(item.value).a(String)) { 286 | if (does(item.value).contain('http://') || does(item.value).contain('https://')) { 287 | item.value = RestClient.request(item.value) 288 | } else { 289 | item.value = fs.createReadStream(path.resolve(item.value)) 290 | } 291 | } 292 | form.append(item.name, item.value, item.options) 293 | } 294 | 295 | return form 296 | } 297 | 298 | if ($this._multipart.length && !$this._stream && $this.options.method != 'get') { 299 | header = $this.options.headers[$this.hasHeader('content-type')] 300 | parts = URL.parse($this.options.url) 301 | form = new FormData() 302 | 303 | if (header) { 304 | $this.options.headers['content-type'] = header.split(';')[0] + '; boundary=' + form.getBoundary() 305 | } else { 306 | $this.options.headers['content-type'] = 'multipart/form-data; boundary=' + form.getBoundary() 307 | } 308 | 309 | return handleFormData(form).submit({ 310 | protocol: parts.protocol, 311 | port: parts.port, 312 | host: parts.hostname, 313 | path: parts.path, 314 | method: $this.options.method, 315 | headers: $this.options.headers 316 | }, function (error, response) { 317 | var decoder = new StringDecoder('utf8') 318 | 319 | if (error) { 320 | return handleRequestResponse(error, response) 321 | } 322 | 323 | if (!response.body) { 324 | response.body = '' 325 | } 326 | 327 | // Node 10+ 328 | response.resume() 329 | 330 | // GZIP, Feel me? 331 | handleGZIPResponse(response) 332 | 333 | // Fallback 334 | response.on('data', function (chunk) { 335 | if (typeof chunk === 'string') response.body += chunk 336 | else response.body += decoder.write(chunk) 337 | }) 338 | 339 | // After all, we end up here 340 | response.on('end', function () { 341 | return handleRequestResponse(error, response) 342 | }) 343 | }) 344 | } 345 | 346 | Request = RestClient.request($this.options, handleRequestResponse) 347 | Request.on('response', handleGZIPResponse) 348 | 349 | if ($this._multipart.length && $this._stream) { 350 | handleFormData(Request.form()) 351 | } 352 | 353 | return Request 354 | } 355 | } 356 | 357 | /** 358 | * Alias for _.header_ 359 | * @type {Function} 360 | */ 361 | $this.headers = $this.header 362 | 363 | /** 364 | * Alias for _.header_ 365 | * 366 | * @type {Function} 367 | */ 368 | $this.set = $this.header 369 | 370 | /** 371 | * Alias for _.end_ 372 | * 373 | * @type {Function} 374 | */ 375 | $this.complete = $this.end 376 | 377 | /** 378 | * Aliases for _.end_ 379 | * 380 | * @type {Object} 381 | */ 382 | 383 | $this.as = { 384 | json: $this.end, 385 | binary: $this.end, 386 | string: $this.end 387 | } 388 | 389 | /** 390 | * Handles Multipart Field Processing 391 | * 392 | * @param {String} name 393 | * @param {Mixed} value 394 | * @param {Object} options 395 | */ 396 | function handleField (name, value, options) { 397 | var serialized 398 | var length 399 | var key 400 | var i 401 | 402 | options = options || { attachment: false } 403 | 404 | if (is(name).a(Object)) { 405 | for (key in name) { 406 | if (name.hasOwnProperty(key)) { 407 | handleField(key, name[key], options) 408 | } 409 | } 410 | } else { 411 | if (is(value).a(Array)) { 412 | for (i = 0, length = value.length; i < length; i++) { 413 | serialized = handleFieldValue(value[i]) 414 | if (serialized) { 415 | $this.rawField(name, serialized, options) 416 | } 417 | } 418 | } else if (value != null) { 419 | $this.rawField(name, handleFieldValue(value), options) 420 | } 421 | } 422 | 423 | return $this 424 | } 425 | 426 | /** 427 | * Handles Multipart Value Processing 428 | * 429 | * @param {Mixed} value 430 | */ 431 | function handleFieldValue (value) { 432 | if (!(value instanceof Buffer || typeof value === 'string')) { 433 | if (is(value).a(Object)) { 434 | if (value instanceof Stream.Readable) { 435 | return value 436 | } else { 437 | return RestClient.serializers.json(value) 438 | } 439 | } else { 440 | return value.toString() 441 | } 442 | } else return value 443 | } 444 | 445 | if (headers && typeof headers === 'function') { 446 | callback = headers 447 | headers = null 448 | } else if (body && typeof body === 'function') { 449 | callback = body 450 | body = null 451 | } 452 | 453 | if (headers) $this.set(headers) 454 | if (body) $this.send(body) 455 | 456 | return callback ? $this.end(callback) : $this 457 | } 458 | 459 | return uri ? restClient(uri, headers, body, callback) : restClient 460 | } 461 | 462 | /** 463 | * Expose the underlying layer. 464 | */ 465 | RestClient.request = require('request') 466 | RestClient.pipe = RestClient.request.pipe 467 | 468 | 469 | RestClient.type = function (type, parse) { 470 | if (typeof type !== 'string') return false 471 | return parse ? type.split(/ *; */).shift() : (RestClient.types[type] || type) 472 | } 473 | 474 | 475 | RestClient.trim = ''.trim 476 | ? function (s) { return s.trim() } 477 | : function (s) { return s.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') } 478 | 479 | RestClient.parsers = { 480 | string: function (data) { 481 | var obj = {} 482 | var pairs = data.split('&') 483 | var parts 484 | var pair 485 | 486 | for (var i = 0, len = pairs.length; i < len; ++i) { 487 | pair = pairs[i] 488 | parts = pair.split('=') 489 | obj[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]) 490 | } 491 | 492 | return obj 493 | }, 494 | 495 | json: function (data) { 496 | try { 497 | data = JSON.parse(data) 498 | } catch (e) {} 499 | 500 | return data 501 | } 502 | } 503 | 504 | /** 505 | * Serialization methods for different data types. 506 | * 507 | * @type {Object} 508 | */ 509 | RestClient.serializers = { 510 | form: function (obj) { 511 | return QueryString.stringify(obj) 512 | }, 513 | 514 | json: function (obj) { 515 | return JSON.stringify(obj) 516 | } 517 | } 518 | 519 | /** 520 | * RestClient Request Utility Methods 521 | * 522 | * @type {Object} 523 | */ 524 | RestClient.Request = { 525 | serialize: function (string, type) { 526 | var serializer = RestClient.firstMatch(type, RestClient.enum.serialize) 527 | return serializer ? serializer(string) : string 528 | } 529 | } 530 | 531 | RestClient.Response = { 532 | parse: function (string, type) { 533 | var parser = RestClient.firstMatch(type, RestClient.enum.parse) 534 | return parser ? parser(string) : string 535 | } 536 | } 537 | 538 | /** 539 | * Enum Structures 540 | * 541 | * @type {Object} 542 | */ 543 | RestClient.enum = { 544 | serialize: { 545 | 'application/x-www-form-urlencoded': RestClient.serializers.form, 546 | 'application/json': RestClient.serializers.json, 547 | 'text/javascript': RestClient.serializers.json 548 | }, 549 | 550 | parse: { 551 | 'application/x-www-form-urlencoded': RestClient.parsers.string, 552 | 'application/json': RestClient.parsers.json, 553 | 'text/javascript': RestClient.parsers.json 554 | }, 555 | 556 | methods: [ 557 | 'GET', 558 | 'HEAD', 559 | 'PUT', 560 | 'POST', 561 | 'PATCH', 562 | 'DELETE', 563 | 'OPTIONS' 564 | ] 565 | } 566 | 567 | RestClient.matches = function matches (string, map) { 568 | var results = [] 569 | 570 | for (var key in map) { 571 | if (typeof map.length !== 'undefined') { 572 | key = map[key] 573 | } 574 | 575 | if (string.indexOf(key) !== -1) { 576 | results.push(map[key]) 577 | } 578 | } 579 | 580 | return results 581 | } 582 | 583 | RestClient.firstMatch = function firstMatch (string, map) { 584 | return RestClient.matches(string, map)[0] 585 | } 586 | 587 | /** 588 | * Generate sugar for request library. 589 | * 590 | * This allows us to mock super-agent chaining style while using request library under the hood. 591 | */ 592 | function setupMethod (method) { 593 | RestClient[method] = RestClient(method) 594 | } 595 | 596 | for (var i = 0; i < RestClient.enum.methods.length; i++) { 597 | var method = RestClient.enum.methods[i].toLowerCase() 598 | setupMethod(method) 599 | } 600 | 601 | function is (value) { 602 | return { 603 | a: function (check) { 604 | if (check.prototype) check = check.prototype.constructor.name 605 | var type = Object.prototype.toString.call(value).slice(8, -1).toLowerCase() 606 | return value != null && type === check.toLowerCase() 607 | } 608 | } 609 | } 610 | 611 | /** 612 | * Simple Utility Methods for checking information about a value. 613 | * 614 | * @param {Mixed} value Could be anything. 615 | * @return {Object} 616 | */ 617 | function does (value) { 618 | var arrayIndexOf = (Array.indexOf ? function (arr, obj, from) { 619 | return arr.indexOf(obj, from) 620 | } : function (arr, obj, from) { 621 | var l = arr.length 622 | var i = from ? parseInt((1 * from) + (from < 0 ? l : 0), 10) : 0 623 | i = i < 0 ? 0 : i 624 | for (; i < l; i++) if (i in arr && arr[i] === obj) return i 625 | return -1 626 | }) 627 | 628 | return { 629 | contain: function (field) { 630 | if (is(value).a(String)) return value.indexOf(field) > -1 631 | if (is(value).a(Object)) return value.hasOwnProperty(field) 632 | if (is(value).a(Array)) return !!~arrayIndexOf(value, field) 633 | return false 634 | } 635 | } 636 | } 637 | 638 | /** 639 | * Expose the RestClient Container 640 | */ 641 | module.exports = exports = RestClient 642 | --------------------------------------------------------------------------------