From e80ba4d90a529f2dbd39c32d4481dbb7ba251504 Mon Sep 17 00:00:00 2001 From: wystan_rin Date: Wed, 13 Nov 2024 16:13:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E7=BD=AE=20Git=20=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E4=B8=8D=E9=9C=80=E8=A6=81=E7=9A=84?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 11 + .gitignore | 3 + payment_backend/.gitignore | 32 ++ payment_backend/README.md | 3 + payment_backend/__init__.py | 0 payment_backend/api/__init__.py | 1 + payment_backend/api/tronscan.py | 411 +++++++++++++++++++++++ payment_backend/app.py | 42 +++ payment_backend/config/__init__.py | 1 + payment_backend/config/db.py | 7 + payment_backend/config/param.ini | 13 + payment_backend/config/utils.py | 145 ++++++++ payment_backend/custom_decorators.py | 17 + payment_backend/database.py | 66 ++++ payment_backend/models.py | 93 +++++ payment_backend/repositories/__init__.py | 0 payment_backend/repositories/order.py | 41 +++ payment_backend/repositories/user.py | 74 ++++ payment_backend/requirements.txt | 3 + payment_backend/services/__init__.py | 0 payment_backend/services/order.py | 58 ++++ payment_backend/services/payment.py | 23 ++ payment_backend/services/user.py | 6 + payment_backend/tests/test_tronscan.py | 35 ++ payment_backend/utils/__init__.py | 0 payment_backend/utils/database.py | 16 + payment_backend/utils/datetime.py | 35 ++ payment_backend/utils/tronscan.py | 2 + payment_headend/.gitignore | 30 ++ payment_headend/index.html | 131 ++++++++ payment_headend/index.js | 1 + payment_headend/package.json | 10 + 32 files changed, 1310 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 payment_backend/.gitignore create mode 100644 payment_backend/README.md create mode 100644 payment_backend/__init__.py create mode 100644 payment_backend/api/__init__.py create mode 100644 payment_backend/api/tronscan.py create mode 100644 payment_backend/app.py create mode 100644 payment_backend/config/__init__.py create mode 100644 payment_backend/config/db.py create mode 100644 payment_backend/config/param.ini create mode 100644 payment_backend/config/utils.py create mode 100644 payment_backend/custom_decorators.py create mode 100644 payment_backend/database.py create mode 100644 payment_backend/models.py create mode 100644 payment_backend/repositories/__init__.py create mode 100644 payment_backend/repositories/order.py create mode 100644 payment_backend/repositories/user.py create mode 100644 payment_backend/requirements.txt create mode 100644 payment_backend/services/__init__.py create mode 100644 payment_backend/services/order.py create mode 100644 payment_backend/services/payment.py create mode 100644 payment_backend/services/user.py create mode 100644 payment_backend/tests/test_tronscan.py create mode 100644 payment_backend/utils/__init__.py create mode 100644 payment_backend/utils/database.py create mode 100644 payment_backend/utils/datetime.py create mode 100644 payment_backend/utils/tronscan.py create mode 100644 payment_headend/.gitignore create mode 100644 payment_headend/index.html create mode 100644 payment_headend/index.js create mode 100644 payment_headend/package.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4991f72 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# 对所有文本文件统一使用 LF 作为行尾字符 +* text=auto eol=lf + +# 指定特定文件使用 CRLF +*.bat text eol=crlf +*.cmd text eol=crlf + +# 二进制文件不进行行尾转换 +*.png binary +*.jpg binary +*.gif binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af68f02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +*.log +/build/ \ No newline at end of file diff --git a/payment_backend/.gitignore b/payment_backend/.gitignore new file mode 100644 index 0000000..a3627a0 --- /dev/null +++ b/payment_backend/.gitignore @@ -0,0 +1,32 @@ +/data/ + +# Python: +*.ipynb +*/__pycache__ +/.vagrant +/scrapy.iml +*.pyc +_trial_temp* +dropin.cache +docs/build +*egg-info +.tox +venv +build +dist +.idea +htmlcov/ +.coverage +.pytest_cache/ +.coverage.* +.cache/ +.mypy_cache/ +/tests/keys/localhost.crt +/tests/keys/localhost.key +key.txt +key.txt.pub + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini diff --git a/payment_backend/README.md b/payment_backend/README.md new file mode 100644 index 0000000..f919462 --- /dev/null +++ b/payment_backend/README.md @@ -0,0 +1,3 @@ +# payment + +支付系统 \ No newline at end of file diff --git a/payment_backend/__init__.py b/payment_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment_backend/api/__init__.py b/payment_backend/api/__init__.py new file mode 100644 index 0000000..9e281b9 --- /dev/null +++ b/payment_backend/api/__init__.py @@ -0,0 +1 @@ +from tronscan import Tronscan diff --git a/payment_backend/api/tronscan.py b/payment_backend/api/tronscan.py new file mode 100644 index 0000000..36395dc --- /dev/null +++ b/payment_backend/api/tronscan.py @@ -0,0 +1,411 @@ +import requests + +from custom_decorators import singleton +from utils.tronscan import convert_to_tronscan_timestamp + +trc20token_info = { + "usdt": {"tokenId": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + "tokenAbbr": "USDT", + "tokenName": "Tether USD", + "tokenLogo": "https://static.tronscan.org/production/logo/usdtlogo.png", + "issuerAddr": "THPvaUhoh2Qn2y9THCZML3H815hhFhn5YC", + "vip": True} +} + +@singleton +class Tronscan: + def __init__(self, api_key): + self.api_key = api_key + + def accountv2(self, address): + """ + Get account detail information + :param address: Account address + :return: Returns the detail information of an account. + """ + response = requests.get(f"https://apilist.tronscanapi.com/api/accountv2?address={address}", + headers={'TRON-PRO-API-KEY': self.api_key}) + return response.json() + + def transactions(self, start=0, limit=10, start_timestamp=None, end_timestamp=None, + from_address=None, to_address=None, tokens=None, block=None, + type_=None, method=None): + """ + Get a list of transactions. + :param start: Start number. Default 0 + :param limit: Number of items per page. Default 10 + :param start_timestamp: Start time + :param end_timestamp: End time + :param from_address: Sender's address. + :param to_address: Recipient's address. + :param tokens: Tokens involved + :param block: Block + :param type_: Transaction type + :param method: Method called in a smart contract signature. Only one value can be specified each time. + :return: Getx a list of transactions. + """ + params = { + "sort": "-timestamp", + "count": "true", + "start": start, + "limit": limit, + } + + if start_timestamp is not None: + params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp) + if end_timestamp is not None: + params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp) + if from_address is not None: + params["fromAddress"] = from_address + if to_address is not None: + params["toAddress"] = to_address + if tokens is not None: + params["tokens"] = tokens + if block is not None: + params["block"] = block + if type_ is not None: + params["type"] = type_ + if method is not None: + params["method"] = method + + response = requests.get( + "https://apilist.tronscanapi.com/api/transaction", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params + ) + return response.json() + + def transaction_info(self, hash_): + """ + Get transaction detail information by transaction hash. + :param hash_: Transaction hash + :return: Get transaction information. + """ + params = { + "hash": hash_, + } + response = requests.get( + "https://apilist.tronscanapi.com/api/transaction-info", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params + ) + return response.json() + + def token_trc20_transfers(self, start=None, limit=None, contract_address=None, + start_timestamp=None, end_timestamp=None, confirm=True, + related_address=None, from_address=None, to_address=None): + """ + Get the transfer list of TRC20 and TRC721 tokens. + :param start: Start number. Default 0 + :param limit: Number of items per page. Default 10 + :param contract_address: Contract address + :param start_timestamp: Start time + :param end_timestamp: End time + :param confirm: Whether to return confirmed transfers only. Default: True + :param related_address: Account address + :param from_address: Sender's address + :param to_address: Recipient's address + :return: Get the transfer list of TRC20 and TRC721 tokens. + """ + params = { + "filterTokenValue": 1 + } + if start is not None: + params["start"] = start + if limit is not None: + params["limit"] = limit + if contract_address is not None: + params["contract_address"] = contract_address + if start_timestamp is not None: + params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp) + if end_timestamp is not None: + params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp) + if confirm is not None: + params["confirm"] = str(confirm).lower() + if related_address is not None: + params["relatedAddress"] = related_address + if from_address is not None: + params["fromAddress"] = from_address + if to_address is not None: + params["toAddress"] = to_address + + response = requests.get( + "https://apilist.tronscanapi.com/api/token_trc20/transfers", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params + ) + return response.json() + + def transfer(self, sort="-timestamp", start=0, limit=10, count="true", + address=None, from_address=None, to_address=None, tokens=None, block=None): + """ + Get account's transfer list. + :param sort: Sort type + :param start: Start index, default is 0 + :param limit: Number of transfers per page + :param count: Whether to return total transfer number. + :param address: Address, like contract address + :param from_address: Sender's address + :param to_address: Recipient's address + :param tokens: Specific tokens + :param block: Block number + :return: Get account's transfer list. + """ + params = { + "sort": sort, + "start": start, + "limit": limit, + "count": count, + "filterTokenValue": 1 + } + + if address is not None: + params["address"] = address + if from_address is not None: + params["fromAddress"] = from_address + if to_address is not None: + params["toAddress"] = to_address + if tokens is not None: + params["tokens"] = tokens + if block is not None: + params["block"] = block + + response = requests.get( + "https://apilist.tronscanapi.com/api/transfer", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params + ) + return response.json() + + def internal_transactions(self, start=0, limit=10, address=None, contract=None, block=None): + """ + Get internal transaction list for a specific address or block. + :param start: Start index, default is 0 + :param limit: Number of transfers per page + :param address: Specific address. At least one of address, block, or contract must be specified + :param contract: Sender's address + :param block: Block number + :return: Get the internal transaction list. + """ + params = { + "start": start, + "limit": limit + } + + if address is not None: + params["address"] = address + if contract is not None: + params["contract"] = contract + if block is not None: + params["block"] = block + + response = requests.get( + "https://apilist.tronscanapi.com/api/internal-transaction", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params + ) + return response.json() + + def token_trc20_transfers_with_status(self, start=0, limit=10, trc20Id=None, address=None, + direction=0, db_version=0, reverse="false"): + """ + Get account's transaction data. + :param start: Start index, default is 0 + :param limit: Number of transfers per page + :param trc20Id: TRC20 token address + :param address: Account address + :param direction: 0 for all, 1 for transfer-out, 2 for transfer-in + :param db_version: Whether to include approval transfers. 1 for include, 0 for exclude + :param reverse: Sort by creation time. Valid values: "true" or "false" + :return: Get account's transaction data. + """ + params = { + "start": start, + "limit": limit, + "direction": direction, + "db_version": db_version, + "reverse": reverse + } + + if trc20Id is not None: + params["trc20Id"] = trc20Id + if address is not None: + params["address"] = address + + response = requests.get( + "https://apilist.tronscanapi.com/api/token_trc20/transfers-with-status", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params + ) + return response.json() + + def search(self, term, type="token", start=0, limit=10): + """ + Search token/contract/account information + Note : The maximum value for limit is 50. + :param term: Search term + :param type: Search type, including "token", "address", "contract", "transaction" and "block" + :param start: Start number. Default: 0 + :param limit: Number of items per page. Default: 10 + :return: Returns account authorization change records. + """ + params = { + "term": term, + "type": type, + "start": start, + "limit": limit, + } + response = requests.get("https://apilist.tronscanapi.com/api/search/v2", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params) + return response.json() + + def approve_change(self, contract_address, from_address, to_address, start=0, limit=20, show=3): + """ + Returns account authorization change records. + :param contract_address: Contract address + :param from_address: Originator address + :param to_address: Recipient address + :param start: Start number. Default: 0 + :param limit: Number of items per page. Default: 20 + :param show: Token type. 1: TRC20 2: TRC721 3: ALL(default) 4: TRC1155 + :return: + """ + params = { + "contract_address": contract_address, + "from_address": from_address, + "to_address": to_address, + "start": start, + "limit": limit, + "show": show, + "type": "approve", + } + response = requests.get("https://apilist.tronscanapi.com/api/account/approve/change", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params) + return response.json() + + def transfer_trx(self, address, start_timestamp=None, end_timestamp=None, start=0, limit=20, direction=1, + db_version=0, + reverse=True, fee=False): + """ + Get the list of trx transfers related to a specific address + Note : The value sum of start and limit must be less than or equal to 10000. + :param address: Query address + :param start_timestamp: Start timestamp + :param end_timestamp: End timestamp + :param start: Start number. Default: 0 + :param limit: Number of items per page. Default: 20 + :param direction: Default: 1. 1 represents inbound transfers, 2 represents outbound transfers, and 0 represents both. + :param db_version: Default: 0, which indicates to filter transfers with invalid “to” or “from” addresses out. + :param reverse: Sort the data in a descending order. Default: true + :param fee: Whether to return data of TRX burning for resource consumption. Default: false + :return: Returns the list of TRX transfers for a specific address. + """ + params = { + "address": address, + "start": start, + "limit": limit, + "direction": direction, + "db_version": db_version, + "reverse": reverse, + "fee": fee, + } + if start_timestamp is not None: + params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp) + if end_timestamp is not None: + params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp) + response = requests.get("https://apilist.tronscanapi.com/api/transfer/trx", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params) + return response.json() + + def transfer_token10(self, address, trc10Id, start_timestamp=None, end_timestamp=None, start=0, limit=20, + direction=1, db_version=0, reverse=True): + """ + Get the transfer list of a specific TRC10 token for a certain address + Note : The value sum of start and limit must be less than or equal to 10000. + :param address: Query address + :param trc10Id: TRC10 token ID + :param start_timestamp: Start timestamp + :param end_timestamp: End timestamp + :param start: Start number. Default: 0 + :param limit: Number of items per page. Default: 20 + :param direction: Default: 1. 1 represents inbound transfers, 2 represents outbound transfers, and 0 represents both. + :param db_version: Default: 0, which indicates to filter transfers with invalid “to” or “from” addresses out. + :param reverse: Sort the data in a descending order. Default: true + :return: Returns the transfer list of a TRC10 token for a specific account. + """ + params = { + "address": address, + "trc10Id": trc10Id, + "start": start, + "limit": limit, + "direction": direction, + "db_version": db_version, + "reverse": reverse, + } + if start_timestamp is not None: + params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp) + if end_timestamp is not None: + params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp) + response = requests.get("https://apilist.tronscanapi.com/api/transfer/token10", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params) + return response.json() + + def transfer_trc20(self, address, trc20Id, start_timestamp=None, end_timestamp=None, start=0, limit=20, + direction=1, db_version=0, reverse=True): + """ + Get the transfer list of a specific TRC20 token for a certain address + Note : The value sum of start and limit must be less than or equal to 10000. + :param address: Query address + :param trc20Id: TRC20 token ID + :param start_timestamp: Start timestamp + :param end_timestamp: End timestamp + :param start: Start number. Default: 0 + :param limit: Number of items per page. Default: 20 + :param direction: Default: 1. 1 represents inbound transfers, 2 represents outbound transfers, and 0 represents both. + :param db_version: Default: 0, which indicates to filter transfers with invalid “to” or “from” addresses out. + :param reverse: Sort the data in a descending order. Default: true + :return: Returns the transfer list of a TRC20 token for a specific account. + """ + params = { + "address": address, + "trc20Id": trc20Id, + "start": start, + "limit": limit, + "direction": direction, + "db_version": db_version, + "reverse": reverse, + } + if start_timestamp is not None: + params["start_timestamp"] = convert_to_tronscan_timestamp(start_timestamp) + if end_timestamp is not None: + params["end_timestamp"] = convert_to_tronscan_timestamp(end_timestamp) + response = requests.get("https://apilist.tronscanapi.com/api/transfer/trc20", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params) + return response.json() + + def account_wallet(self, address, asset_type=0): + """ + Get the information of tokens held and followed in the account's web wallet + :param address: Query address + :param asset_type: Asset types: 0 - All (default); 1 - Assets (TRX, TRC10, TRC20); 2 - Collectibles (TRC721 and TRC1155) + :return: Returns a list of tokens held and followed by an account. + """ + params = { + "address": address, + "asset_type": asset_type, + } + response = requests.get("https://apilist.tronscanapi.com/api/account/wallet", + headers={'TRON-PRO-API-KEY': self.api_key}, + params=params) + return response.json() + + +if __name__ == '__main__': + address = "TB592A5QwHvvcJoCmvALmzT3S9Pux91Gub" + tronscan = Tronscan(api_key='cc87d361-7cd6-4f69-a57b-f0a77a213355') + print(tronscan.transfer_trc20(address, trc20token_info["usdt"]["tokenId"])) diff --git a/payment_backend/app.py b/payment_backend/app.py new file mode 100644 index 0000000..48aa46c --- /dev/null +++ b/payment_backend/app.py @@ -0,0 +1,42 @@ +from flask import Flask, request, jsonify + +from config import get_config +from services.order import OrderService + +config = get_config() + +app = Flask(__name__) +order_service = OrderService() # 获取单例实例 + +@app.route('/createOrder', methods=['POST']) +def create_order(): + data = request.get_json() + phone = data.get('phone', None) + email = data.get('email', None) + address = data.get('address', None) + try: + payment_method = data['paymentMethod'] + except KeyError: + return jsonify({ + "message": "Unsupported payment method. Currently, only USDT payments are supported." + }), 400 + + addresses = order_service.get_user_addresses(phone, email, address, payment_method) + + if not addresses: + return jsonify({ + "message": "No payment address associated with you was found. Please provide a payment address." + }), 400 + + if len(addresses) == 1: + order_id = order_service.create_order(addresses[0]) + return jsonify({"order_id": order_id}), 200 + + # 多个地址的情况 + return jsonify({ + "message": "请选择一个地址进行下单。", + "addresses": addresses + }), 200 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/payment_backend/config/__init__.py b/payment_backend/config/__init__.py new file mode 100644 index 0000000..1f12718 --- /dev/null +++ b/payment_backend/config/__init__.py @@ -0,0 +1 @@ +from .utils import get_config diff --git a/payment_backend/config/db.py b/payment_backend/config/db.py new file mode 100644 index 0000000..595714f --- /dev/null +++ b/payment_backend/config/db.py @@ -0,0 +1,7 @@ +config = { + 'user': 'your_mysql_username', + 'password': 'your_mysql_password', + 'host': 'localhost', + 'database': 'your_database_name', + 'autocommit': False, +} diff --git a/payment_backend/config/param.ini b/payment_backend/config/param.ini new file mode 100644 index 0000000..93bc4de --- /dev/null +++ b/payment_backend/config/param.ini @@ -0,0 +1,13 @@ +[APIKey] +tronscan=cc87d361-7cd6-4f69-a57b-f0a77a213355 + +[PaymentAddresses] +usdt=TB592A5QwHvvcJoCmvALmzT3S9Pux91Gub + +[MYSQL] +user: 'your_mysql_username' +password: 'your_mysql_password' +host: 'localhost' +database: 'your_database_name' +autocommit: false +allow_multi_statements: True \ No newline at end of file diff --git a/payment_backend/config/utils.py b/payment_backend/config/utils.py new file mode 100644 index 0000000..4a6d836 --- /dev/null +++ b/payment_backend/config/utils.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :payment +@File :__init__.py.py +@IDE :PyCharm +@Author :rengengchen +@Time :2024/11/06 16:11 +""" +import argparse +import os +import random +import sys +from argparse import Namespace +from configparser import ConfigParser + +import requests +from loguru import logger + +random_seed = 20240717 +ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + + +def setup_seed(seed): + # import torch + # torch.manual_seed(seed) + # torch.cuda.manual_seed_all(seed) + # torch.backends.cudnn.deterministic = True + # import numpy as np + # np.random.seed(seed) + random.seed(seed) + + +setup_seed(random_seed) + + +class Setting: + def __init__(self, + config_parser: ConfigParser = None, + argument_parser: Namespace = None, + _visited=None, + _parent=None, + **kwargs): + self._parent = _parent + if config_parser: + for section in config_parser.sections(): + section_config = Setting(_parent=self) + for option in config_parser.options(section): + section_config[option] = config_parser.get(section, option) + self.__dict__[section] = section_config + + if argument_parser: + for k in vars(argument_parser): + self.__dict__[k] = getattr(argument_parser, k) + + if _visited is None: + _visited = set() + self.update(_visited=_visited, **kwargs) + + def update(self, _visited=None, **kwargs): + if _visited is None: + _visited = {} + for k, v in kwargs.items(): + cls_attr = getattr(self.__class__, k, None) + if callable(cls_attr): + raise KeyError(f"The key '{k}' conflicts with an existing class method. you can use {k}_ instead.") + if isinstance(v, dict): + obj_id = id(v) + if obj_id in _visited: + logger.warning(f"Circular reference detected in key: '{k}'.") + v = _visited[obj_id] + else: + v = Setting(_visited=_visited, _parent=self, **v) + _visited[obj_id] = v + self.__dict__[k] = v + + def get(self, item, default=None): + if item not in self.__dict__ and self._parent is not None: + return self._parent.get(item, default) + return default + + def get_int(self, item): + return int(self.get(item)) + + def get_float(self, item): + return float(self.get(item)) + + def get_bool(self, item): + return bool(self.get(item)) + + def set(self, item, value): + self.__dict__[item] = value + + def __getitem__(self, item): + return self.__dict__[item] + + def __setitem__(self, key, value): + self.__dict__[key] = value + + def __getattr__(self, key): + return self.get(key) + + def __str__(self): + def _str_helper(config, indent=0, visited=None): + if visited is None: + visited = set() + lines = [] + indent_str = ' ' * indent + for key, value in config.__dict__.items(): + if key.startswith('_'): + continue + if isinstance(value, Setting): + if id(value) in visited: + lines.append(f"{indent_str}{key}: ") + else: + visited.add(id(value)) + lines.append(f"{indent_str}{key}:") + lines.append(_str_helper(value, indent + 1, visited)) + else: + lines.append(f"{indent_str}{key}: {value}") + return '\n'.join(lines) + + return _str_helper(self) + + +def log_config(config): + # fmt = '%(asctime)s [%(name)s] %(levelname)s: %(message)s' + # datefmt = "%Y-%m-%d %H:%M:%S" + logger.remove() + logger.add(sys.stdout, level=config.log_level) + logger.add(sys.stderr, level="ERROR") + logger.add(os.path.join(ROOT_DIR, "logs", "{time}.log"), level="DEBUG", encoding='utf8', rotation="100 MB", + retention=3) + + +def get_config(config_file=fr'{ROOT_DIR}/config/param.ini') -> Setting: + requests.adapters.DEFAULT_RETRIES = 3 + + configparser = ConfigParser() + configparser.read(config_file) + parser = argparse.ArgumentParser(description='payment system') + parser.add_argument("--seed", type=int, default=2024) + args = parser.parse_args() + config = Setting(configparser, args) + return config diff --git a/payment_backend/custom_decorators.py b/payment_backend/custom_decorators.py new file mode 100644 index 0000000..947e4e8 --- /dev/null +++ b/payment_backend/custom_decorators.py @@ -0,0 +1,17 @@ +def singleton(cls): + """ + Decorator for making a class a singleton. + This ensures that only one instance of the class exists. + """ + instances = {} # Dictionary to store the instance of the singleton class + + def get_instance(*args, **kwargs): + """ + If an instance of the class does not exist, create one and store it. + If it exists, return the existing instance. + """ + if cls not in instances: + instances[cls] = cls(*args, **kwargs) + return instances[cls] + + return get_instance diff --git a/payment_backend/database.py b/payment_backend/database.py new file mode 100644 index 0000000..b6758c2 --- /dev/null +++ b/payment_backend/database.py @@ -0,0 +1,66 @@ +from loguru import logger +from mysql.connector import connect, Error, OperationalError +from mysql.connector import errors as db_errors + +from custom_decorators import singleton + + +@singleton +class Database: + def __init__(self, config): + self.connection = None + self.config = config + self.connect() + + def connect(self): + """Establish a new database connection.""" + try: + self.connection = connect(**self.config) + if self.connection.is_connected(): + logger.info("Connected to MySQL database") + except Error as e: + logger.info(f"Error while connecting to MySQL: {e}") + self.connection = None + + def get_connection(self): + """Get the database connection, with reconnection logic.""" + if self.connection is None or not self.connection.is_connected(): + logger.info("Reconnecting to the database...") + self.connect() + return self.connection + + def close_connection(self): + if self.connection and self.connection.is_connected(): + self.connection.close() + logger.info("MySQL connection is closed") + + def execute_query(self, query, params=None): + """Execute a query with optional parameters, supports transactions.""" + cursor = None + try: + connection = self.get_connection() + cursor = connection.cursor() + cursor.execute(query, params) + return cursor + except OperationalError as e: + logger.info(f"Operational error: {e}. Attempting to reconnect...") + self.connect() + cursor = self.get_connection().cursor() + cursor.execute(query, params) + return cursor + except db_errors.Error as e: + logger.info(f"Database error: {e}") + raise + finally: + if cursor: + cursor.close() + + def commit(self): + """Commit the current transaction.""" + if self.connection: + self.connection.commit() + + def rollback(self): + """Rollback the current transaction.""" + if self.connection: + self.connection.rollback() diff --git a/payment_backend/models.py b/payment_backend/models.py new file mode 100644 index 0000000..1dd08ea --- /dev/null +++ b/payment_backend/models.py @@ -0,0 +1,93 @@ +from utils.database import pack_params + + +class User: + def __init__(self, id_=None, name=None, phone=None, email=None, address=None, payment_method=None): + self.id = id_ + self.name = name + self.phone = phone + self.email = email + self.address = address + self.payment_method = payment_method + + def insert_sql(self, params_format="list"): + params_sql, params = pack_params(params_format=params_format, param_sql="{param}", join_str=",", + name=self.name, phone=self.phone, email=self.email, address=self.address, + payment_method=self.payment_method) + return f"INSERT INTO user ({params_sql}) VALUES ({','.join('%s' for _ in params)})", params + + def select_sql(self, condition="AND", params_format="list"): + params_sql, params = pack_params(params_format=params_format, param_sql="{param}=%s", join_str=f" {condition} ", + name=self.name, phone=self.phone, email=self.email, address=self.address, + payment_method=self.payment_method) + return f"SELECT id, name, phone, email, address, payment_method FROM user WHERE {params_sql}", params + + def exists_sql(self, condition="AND", params_format="list"): + params_sql, params = pack_params(params_format=params_format, param_sql="{param}=%s", join_str=f" {condition} ", + name=self.name, phone=self.phone, email=self.email, address=self.address, + payment_method=self.payment_method) + return f"SELECT id FROM user WHERE {params_sql} LIMIT 1", params + + def params(self, format="dict"): + if format == "list": + params = [] + elif format == "dict": + params = {} + else: + raise ValueError("format must be list or dict") + if self.uid: + if format == "list": + params.append(self.uid) + elif format == "dict": + params["uid"] = self.uid + if self.name: + if format == "list": + params.append(self.name) + elif format == "dict": + params["name"] = self.name + if self.phone: + if format == "list": + params.append(self.phone) + elif format == "dict": + params["phone"] = self.phone + if self.email: + if format == "list": + params.append(self.email) + elif format == "dict": + params["email"] = self.email + if self.address: + if format == "list": + params.append(self.address) + elif format == "dict": + params["address"] = self.address + if self.payment_method: + if format == "list": + params.append(self.payment_method) + elif format == "dict": + params["payment_method"] = self.payment_method + return params + + def get_difference(self, other_user): + different_attrs = {} + if self.uid != other_user.uid: + different_attrs["uid"] = (self.uid, other_user.uid) + if self.name != other_user.name: + different_attrs["name"] = (self.name, other_user.name) + if self.phone != other_user.phone: + different_attrs["phone"] = (self.phone, other_user.phone) + if self.email != other_user.email: + different_attrs["email"] = (self.email, other_user.email) + if self.address != other_user.address: + different_attrs["address"] = (self.address, other_user.address) + if self.payment_method != other_user.payment_method: + different_attrs["payment_method"] = (self.payment_method, other_user.payment_method) + return different_attrs + + def __eq__(self, other): + if isinstance(other, User): + return ((self.name, self.phone, self.email, self.address, self.payment_method) + == (other.name, other.phone, other.email, other.address, other.payment_method)) + return False + + def __hash__(self): + return hash((self.name, self.phone, self.email, self.address, self.payment_method)) diff --git a/payment_backend/repositories/__init__.py b/payment_backend/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment_backend/repositories/order.py b/payment_backend/repositories/order.py new file mode 100644 index 0000000..ac92ad8 --- /dev/null +++ b/payment_backend/repositories/order.py @@ -0,0 +1,41 @@ +from ruamel_yaml.util import create_timestamp + +from custom_decorators import singleton +from database import Database +from utils.datetime import current_timestamp + + +@singleton +class OrderRepository: + def __init__(self, config): + self.db = Database(config['MYSQL']) + + def create(self, order_id, from_address, to_address): + cur_time = current_timestamp() + try: + self.db.execute_query( + "INSERT INTO orders (order_id, from_address, to_address, create_timestamp, update_timestamp) " + "VALUES (%s, %s, %s, %s, %s)", + [order_id, from_address, to_address, cur_time, cur_time] + ) + self.db.commit() + except Exception: + self.db.rollback() + raise + + def update_status(self, order_id, status): + try: + self.db.execute_query("UPDATE orders " + "SET status = %s, update_timestamp = %s " + "WHERE order_id = %s", + [status, current_timestamp(), order_id]) + self.db.commit() + except Exception: + self.db.rollback() + raise + + def get_order_info(self, order_id): + self.db.execute_query("SELECT quant, from_address, to_address, create_timestamp " + "FROM orders " + "WHERE order_id = %s", + [order_id]) diff --git a/payment_backend/repositories/user.py b/payment_backend/repositories/user.py new file mode 100644 index 0000000..b580fdc --- /dev/null +++ b/payment_backend/repositories/user.py @@ -0,0 +1,74 @@ +import itertools + +from custom_decorators import singleton +from database import Database +from models import User + + +@singleton +class UserRepository: + def __init__(self, config): + self.db = Database(config['MYSQL']) + + def get_or_create(self, user): + users = [] + cursor = self.db.execute_query(*user.select_sql(condition="OR")) + same_users = cursor.fetchall() + new_user = not len(same_users) + # 对用户已存在的属性判断是否有新属性 + update_user = set() + update_sqls = [] + update_params_list = [] + delete_params = [] + exist_conflicting_attr = False + for same_user in same_users: + exist_conflicting_attr = False + different_attrs = user.get_difference(same_user) + # 用于判断是否有新属性 + update_sql_params = [] + update_params = [] + for k, v in different_attrs.items(): + new_attr, exist_attr = v + if exist_attr is None: + setattr(same_user, k, new_attr) + update_sql_params.append(f"{k}=%s") + update_params.append(new_attr) + else: + # 出现冲突的属性,考虑新增一行记录 + exist_conflicting_attr = True + break + + if same_user in update_user: + delete_params.append((same_user.id,)) + else: + users.append(same_user) + exist_new_attr = bool(update_params) + if exist_new_attr: + update_user.add(same_user) + update_sqls.append(f'UPDATE user SET {",".join(update_sql_params)} WHERE id=%s;') + update_params.append(same_user.id) + update_params_list.append(update_params) + sql_flag = False + try: + if delete_params: + sql_flag = True + self.db.get_connection().cursor().executemany("DELETE FROM user WHERE id=%s", delete_params) + if update_user: + sql_flag = True + self.db.get_connection().cursor().execute("".join(update_sqls), + list(itertools.chain.from_iterable(update_params_list)), + multi=True) + if sql_flag: + self.db.commit() + except Exception: + self.db.rollback() + raise + if new_user or exist_conflicting_attr: + try: + self.db.execute_query(*user.insert_sql()) + self.db.commit() + except Exception: + self.db.rollback() + raise + users.append(user) + return users diff --git a/payment_backend/requirements.txt b/payment_backend/requirements.txt new file mode 100644 index 0000000..1e21fb7 --- /dev/null +++ b/payment_backend/requirements.txt @@ -0,0 +1,3 @@ +loguru==0.7.2 +mysql-connector-python==9.1.0 +Requests==2.32.3 diff --git a/payment_backend/services/__init__.py b/payment_backend/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment_backend/services/order.py b/payment_backend/services/order.py new file mode 100644 index 0000000..1272db4 --- /dev/null +++ b/payment_backend/services/order.py @@ -0,0 +1,58 @@ +import uuid + +from custom_decorators import singleton +from models import User +from repositories.order import OrderRepository +from repositories.user import UserRepository +from services.payment import PaymentService +from utils.datetime import current, current_timestamp, is_time_difference_greater_than + + +@singleton +class OrderService: + def __init__(self, config): + self.config = config + self.payment_service = PaymentService() + self.order_repo = OrderRepository(config) + self.user_repo = UserRepository(config) + + def get_user_addresses(self, phone=None, email=None, address=None, payment_method=None): + if address is None: + if phone or email: + users = self.user_repo.get_or_create(User(phone=phone, email=email)) + addresses = set(user.address for user in users if address) + return list(addresses) + raise ValueError('A phone number, email, or address is required.') + return [address] + + def create_order(self, address=None): + date_str = current().strftime('%Y%m%d%H%M%S') + unique_id = str(uuid.uuid4()).split('-')[0] + order_id = f"{date_str}-{unique_id}" + + self.order_repo.create(order_id, address, + self.config['PaymentAddresses']) + return order_id + + def finish_order(self, order_id): + # 判断支付时间是否超过订单存活时间 + quant, from_address, to_address, create_timestamp = self.order_repo.get_order_info(order_id) + current = current_timestamp() + status = 0 + if is_time_difference_greater_than(create_timestamp, current, minutes=15): + # 订单超时 + status = 4 + else: + correct_quant, confirmed = self.payment_service.check_payment(quant, from_address, to_address, create_timestamp, current) + if correct_quant and confirmed: + # 支付成功 + status = 1 + elif correct_quant < 0: + # 没有转账 + status = 2 + elif confirmed: + # 金额不对 + status = 3 + if status: + self.order_repo.update_status(order_id, status) + return status diff --git a/payment_backend/services/payment.py b/payment_backend/services/payment.py new file mode 100644 index 0000000..b602376 --- /dev/null +++ b/payment_backend/services/payment.py @@ -0,0 +1,23 @@ +from api import Tronscan +from custom_decorators import singleton +from utils.datetime import current_timestamp + + +@singleton +class PaymentService: + def __init__(self, api_key): + self.tronscan = Tronscan(api_key) + + def check_payment(self, quant, from_address, to_address, order_create_timestamp, end_timestamp=None): + if end_timestamp is None: + end_timestamp = current_timestamp() + result = self.tronscan.token_trc20_transfers(limit=100, + from_address=from_address, to_address=to_address, + start_timestamp=order_create_timestamp, end_timestamp=end_timestamp) + if result['rangeTotal'] == 0: + return -1, 0 + token_transfers = result['token_transfers'] + token_transfer = token_transfers[-1] + confirmed = token_transfer['confirmed'] + correct_quant = int(quant == (token_transfer['quant'] / 6)) + return correct_quant, confirmed diff --git a/payment_backend/services/user.py b/payment_backend/services/user.py new file mode 100644 index 0000000..7fbae0d --- /dev/null +++ b/payment_backend/services/user.py @@ -0,0 +1,6 @@ +from custom_decorators import singleton + + +@singleton +class UserService: + pass diff --git a/payment_backend/tests/test_tronscan.py b/payment_backend/tests/test_tronscan.py new file mode 100644 index 0000000..f558fcd --- /dev/null +++ b/payment_backend/tests/test_tronscan.py @@ -0,0 +1,35 @@ +import unittest +from unittest.mock import patch + +from api import Tronscan + + +class TestExternalAPICalls(unittest.TestCase): + + def setUp(self): + # Setup code runs before every test method + self.tronscan = Tronscan(api_key='cc87d361-7cd6-4f69-a57b-f0a77a213355') + + @unittest.skip("Skipping this test temporarily") + @patch('requests.get') + def test_real_api_call(self, mock_get): + # Mocking API response + mock_response = { + "data": [{"name": "ExampleToken", "symbol": "ETK"}] + } + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = mock_response + + # Call the method + response = self.tronscan.search(term="example", type="token") + + # Assert the response + self.assertEqual(response, mock_response) + mock_get.assert_called_once_with( + "https://apilist.tronscanapi.com/api/search/v2", + headers={'TRON-PRO-API-KEY': self.api_key}, + params={"term": "example", "type": "token", "start": 0, "limit": 10} + ) + +if __name__ == "__main__": + unittest.main() diff --git a/payment_backend/utils/__init__.py b/payment_backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment_backend/utils/database.py b/payment_backend/utils/database.py new file mode 100644 index 0000000..f173ff3 --- /dev/null +++ b/payment_backend/utils/database.py @@ -0,0 +1,16 @@ +def pack_params(params_format="list", param_sql="{param}=%s", join_str=" AND ", **kwargs): + if params_format == "list": + params = [] + elif params_format == "dict": + params = {} + else: + raise ValueError("Unknown params format") + param_sqls = [] + for k, v in kwargs.items(): + if v is not None: + if params_format == "list": + params.append(v) + elif params_format == "dict": + params[k] = v + param_sqls.append(param_sql.format(param=k)) + return join_str.join(param_sqls), params diff --git a/payment_backend/utils/datetime.py b/payment_backend/utils/datetime.py new file mode 100644 index 0000000..40a2c50 --- /dev/null +++ b/payment_backend/utils/datetime.py @@ -0,0 +1,35 @@ +import datetime + + +def current(): + return datetime.datetime.now() + +def current_timestamp(): + datetime.datetime.now().timestamp() + +def is_time_difference_greater_than(timestamp1, timestamp2, hours=0, minutes=0, seconds=0): + """ + 判断两个时间戳的时间差是否大于指定的小时、分钟和秒数 + + 参数: + timestamp1 (int): 第一个时间戳 + timestamp2 (int): 第二个时间戳 + hours (int): 要比较的小时数,默认是0小时 + minutes (int): 要比较的分钟数,默认是0分钟 + seconds (int): 要比较的秒数,默认是0秒 + + 返回: + bool: 如果时间差大于指定的小时、分钟和秒数返回True,否则返回False + """ + # 将时间戳转换为 datetime 对象 + time1 = datetime.fromtimestamp(timestamp1) + time2 = datetime.fromtimestamp(timestamp2) + + # 计算时间差 + time_difference = abs(time2 - time1) + + # 计算指定的时间差值 + threshold = datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds) + + # 判断时间差是否大于指定的时间 + return time_difference > threshold \ No newline at end of file diff --git a/payment_backend/utils/tronscan.py b/payment_backend/utils/tronscan.py new file mode 100644 index 0000000..65310b4 --- /dev/null +++ b/payment_backend/utils/tronscan.py @@ -0,0 +1,2 @@ +def convert_to_tronscan_timestamp(timestamp): + return int(timestamp * 1000) diff --git a/payment_headend/.gitignore b/payment_headend/.gitignore new file mode 100644 index 0000000..99d8734 --- /dev/null +++ b/payment_headend/.gitignore @@ -0,0 +1,30 @@ +# Python: +*.ipynb +*/__pycache__ +/.vagrant +/scrapy.iml +*.pyc +_trial_temp* +dropin.cache +docs/build +*egg-info +.tox +venv +build +dist +.idea +htmlcov/ +.coverage +.pytest_cache/ +.coverage.* +.cache/ +.mypy_cache/ +/tests/keys/localhost.crt +/tests/keys/localhost.key +key.txt +key.txt.pub + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini diff --git a/payment_headend/index.html b/payment_headend/index.html new file mode 100644 index 0000000..432556d --- /dev/null +++ b/payment_headend/index.html @@ -0,0 +1,131 @@ + + + + + 创建订单 + + + +

创建订单

+
+ +

+ + +

+ + +

+ + +
+ + + + + + + + diff --git a/payment_headend/index.js b/payment_headend/index.js new file mode 100644 index 0000000..04bf26e --- /dev/null +++ b/payment_headend/index.js @@ -0,0 +1 @@ +console.log('Happy developing ✨') diff --git a/payment_headend/package.json b/payment_headend/package.json new file mode 100644 index 0000000..340c6a1 --- /dev/null +++ b/payment_headend/package.json @@ -0,0 +1,10 @@ +{ + "name": "payment_headend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "private": true +}