分类: 聊聊技术

16 条动态
Leo

区块链系列3 – 如何发布支持预授权的ERC20代币

接着上一篇“区块链系列2 – 如何发布一个支持黑名单管理的ERC20代币” ,对于发币来说,默认的只支持主动转账的ERC20代币有时会有限制,在传统银行业务中,还有一个场景是授权交易,比如A账户可以发起一个授权,允许B账户在一定时间内向自己的账户发起扣款X元,这里的B账户、时间范围、累计扣款金额就是三个参数,那是否可以基于区块链开发一个这样的智能合约?即允许一个钱包用户授权另一个钱包客户在一定时间范围内一定金额范围内向自己的钱包发起扣款。经过我尝试,是可以的。

智能合约

预授权功能实现

重点内容:自托管钱包比如MetaMask它只有基础的转账和收款功能,如果是一个授权,是需要调用智能合约的一个方法,MetaMask是做不到的,所以就需要一个独立的DApp才可以,在自己开发的DApp里主动让钱包的私钥签名后调用特定的授权方法。

DApp开发

现在都有很成熟的DApp开发框架,很多语言也都可以实现,为了实现方式,我才用了php。

开发好的网址是: https://token.liangdong.me ,如下图所示

选择链

管理员权限

MetaMask选择测试链,并选择发布合约相同的钱包,即可看到管理员的功能,可以看到有100万代币,精读是2。

创建预授权

现在准备验证发起预授权,首先使用另外一个钱包, 0x207C3fF50e568a6DfEFB34FdDAd36faB69648Aa6 ,先使用管理员钱包0xDb5206169c8c491BC8B17d294DbaFF6644df889B 向这个钱包铸币1000

切换到0x207C3fF50e568a6DfEFB34FdDAd36faB69648Aa6钱包后即可看到余额

现在就可以发起预授权,授权0xDb5206169c8c491BC8B17d294DbaFF6644df889B钱包可以在1小时内授权累计500,不区分次数,总额不超过500即可。

点击创建预授权时需要通过Meta Mask签名,签名时也会看到信息,

Authorize 0xDb5206169c8c491BC8B17d294DbaFF6644df889B to spend 50000 wei of IOCT4CM. Valid for 1 hours.

这笔签名需要Gas Fee

预授权成功

可查询到该笔预授权,关键信息时预授权ID,后续可以通过这个预授权ID发起扣款交易,当然前提是只有允许被授权的钱包0xDb5206169c8c491BC8B17d294DbaFF6644df889B才可以通过这个ID发起授权扣款,其他钱包不可以。

发起预授权

现在把MetaMask切回到可以发起预授权的钱包0xDb5206169c8c491BC8B17d294DbaFF6644df889B ,输入预授权ID,输入金额100,点击执行扣款。

授权完成后各钱包查询

授权钱包余额900

Leo

区块链系列2 – 如何发布一个支持黑名单管理的ERC20代币

接着上一篇“区块链系列1 – 如何在以太坊发布一个带图标带官网的ERC20代币” ,对于发币来说,肯定希望增加黑名单管理的功能,比如动态增加黑名单、删除黑名单,这又分为付款黑名单 和 收款黑名单,这就会有几个问题。

  • 黑名单保存在哪里?
  • 可以通过一个URL来配置黑名单吗?
  • MetaMask这样的Web3 Wallet可以使用这种ERC20代币吗?
  • 在智能合约里配置因黑名单造成转账失败的错误提示,可以被查看吗?

一、Talk is cheap. Show me the code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ERC20BlackList is ERC20, Ownable {
    // 黑名单结构体,包含状态和时间信息
    struct BlackListInfo {
        bool isBlacklisted;
        uint256 addedTime;
        uint256 removedTime;
    }
    
    mapping(address => BlackListInfo) public blackPayerList;
    mapping(address => BlackListInfo) public blackPayeeList;
    
    event BlackPayerAdded(address indexed account, uint256 addedTime);
    event BlackPayerRemoved(address indexed account, uint256 removedTime);
    event BlackPayeeAdded(address indexed account, uint256 addedTime);
    event BlackPayeeRemoved(address indexed account, uint256 removedTime);
    
    constructor() ERC20("IOC Team Token V2 BlackList", "IOCV2BL") Ownable(msg.sender) {
        _mint(msg.sender, 10000 * 10**decimals());
    }
    
    function _update(address from, address to, uint256 value) internal override {
        // 检查付款人黑名单
        if (from != address(0)) {
            require(!blackPayerList[from].isBlacklisted, "Transfer failed: sender address is blacklisted");
        }
        
        // 检查收款人黑名单
        if (to != address(0)) {
            require(!blackPayeeList[to].isBlacklisted, "Transfer failed: recipient address is blacklisted");
        }
        
        super._update(from, to, value);
    }
    
    // =============================================================
    // 付款人黑名单管理功能
    // =============================================================
    
    // 添加单个地址到付款人黑名单
    function addBlackPayer(address account) external onlyOwner {
        require(account != address(0), "Cannot add zero address");
        require(!blackPayerList[account].isBlacklisted, "Address already in payer blacklist");
        
        blackPayerList[account].isBlacklisted = true;
        blackPayerList[account].addedTime = block.timestamp;
        
        emit BlackPayerAdded(account, block.timestamp);
    }
    
    // 移除单个地址从付款人黑名单
    function removeBlackPayer(address account) external onlyOwner {
        require(blackPayerList[account].isBlacklisted, "Address not in payer blacklist");
        
        blackPayerList[account].isBlacklisted = false;
        blackPayerList[account].removedTime = block.timestamp;
        
        emit BlackPayerRemoved(account, block.timestamp);
    }
    
    // 批量添加付款人黑名单
    function batchAddBlackPayers(address[] memory accounts) external onlyOwner {
        require(accounts.length > 0, "Address list cannot be empty");
        uint256 currentTime = block.timestamp;
        
        for (uint256 i = 0; i < accounts.length; i++) {
            address account = accounts[i];
            require(account != address(0), "Cannot add zero address");
            
            if (!blackPayerList[account].isBlacklisted) {
                blackPayerList[account].isBlacklisted = true;
                blackPayerList[account].addedTime = currentTime;
                emit BlackPayerAdded(account, currentTime);
            }
        }
    }
    
    // 批量移除付款人黑名单
    function batchRemoveBlackPayers(address[] memory accounts) external onlyOwner {
        require(accounts.length > 0, "Address list cannot be empty");
        uint256 currentTime = block.timestamp;
        
        for (uint256 i = 0; i < accounts.length; i++) {
            address account = accounts[i];
            
            if (blackPayerList[account].isBlacklisted) {
                blackPayerList[account].isBlacklisted = false;
                blackPayerList[account].removedTime = currentTime;
                emit BlackPayerRemoved(account, currentTime);
            }
        }
    }
    
    // =============================================================
    // 收款人黑名单管理功能
    // =============================================================
    
    // 添加单个地址到收款人黑名单
    function addBlackPayee(address account) external onlyOwner {
        require(account != address(0), "Cannot add zero address");
        require(!blackPayeeList[account].isBlacklisted, "Address already in payee blacklist");
        
        blackPayeeList[account].isBlacklisted = true;
        blackPayeeList[account].addedTime = block.timestamp;
        
        emit BlackPayeeAdded(account, block.timestamp);
    }
    
    // 移除单个地址从收款人黑名单
    function removeBlackPayee(address account) external onlyOwner {
        require(blackPayeeList[account].isBlacklisted, "Address not in payee blacklist");
        
        blackPayeeList[account].isBlacklisted = false;
        blackPayeeList[account].removedTime = block.timestamp;
        
        emit BlackPayeeRemoved(account, block.timestamp);
    }
    
    // 批量添加收款人黑名单
    function batchAddBlackPayees(address[] memory accounts) external onlyOwner {
        require(accounts.length > 0, "Address list cannot be empty");
        uint256 currentTime = block.timestamp;
        
        for (uint256 i = 0; i < accounts.length; i++) {
            address account = accounts[i];
            require(account != address(0), "Cannot add zero address");
            
            if (!blackPayeeList[account].isBlacklisted) {
                blackPayeeList[account].isBlacklisted = true;
                blackPayeeList[account].addedTime = currentTime;
                emit BlackPayeeAdded(account, currentTime);
            }
        }
    }
    
    // 批量移除收款人黑名单
    function batchRemoveBlackPayees(address[] memory accounts) external onlyOwner {
        require(accounts.length > 0, "Address list cannot be empty");
        uint256 currentTime = block.timestamp;
        
        for (uint256 i = 0; i < accounts.length; i++) {
            address account = accounts[i];
            
            if (blackPayeeList[account].isBlacklisted) {
                blackPayeeList[account].isBlacklisted = false;
                blackPayeeList[account].removedTime = currentTime;
                emit BlackPayeeRemoved(account, currentTime);
            }
        }
    }
    
    // =============================================================
    // 黑名单检查功能(带时间信息)
    // =============================================================
    
    // 检查地址是否在付款人黑名单中
    function isBlackPayer(address account) external view returns (bool) {
        return blackPayerList[account].isBlacklisted;
    }
    
    // 检查地址是否在收款人黑名单中
    function isBlackPayee(address account) external view returns (bool) {
        return blackPayeeList[account].isBlacklisted;
    }
    
    // 获取付款人黑名单详细信息
    function getBlackPayerInfo(address account) external view returns (
        bool isBlacklisted,
        uint256 addedTime,
        uint256 removedTime
    ) {
        BlackListInfo memory info = blackPayerList[account];
        return (info.isBlacklisted, info.addedTime, info.removedTime);
    }
    
    // 获取收款人黑名单详细信息
    function getBlackPayeeInfo(address account) external view returns (
        bool isBlacklisted,
        uint256 addedTime,
        uint256 removedTime
    ) {
        BlackListInfo memory info = blackPayeeList[account];
        return (info.isBlacklisted, info.addedTime, info.removedTime);
    }
    
    // 检查地址在两个黑名单中的状态
    function getBlackListStatus(address account) external view returns (
        bool isPayerBlacklisted,
        bool isPayeeBlacklisted
    ) {
        return (
            blackPayerList[account].isBlacklisted,
            blackPayeeList[account].isBlacklisted
        );
    }
    
    // 获取地址的完整黑名单信息
    function getFullBlackListInfo(address account) external view returns (
        bool isPayerBlacklisted,
        uint256 payerAddedTime,
        uint256 payerRemovedTime,
        bool isPayeeBlacklisted,
        uint256 payeeAddedTime,
        uint256 payeeRemovedTime
    ) {
        BlackListInfo memory payerInfo = blackPayerList[account];
        BlackListInfo memory payeeInfo = blackPayeeList[account];
        
        return (
            payerInfo.isBlacklisted,
            payerInfo.addedTime,
            payerInfo.removedTime,
            payeeInfo.isBlacklisted,
            payeeInfo.addedTime,
            payeeInfo.removedTime
        );
    }
    
    // 批量检查多个地址的付款人黑名单状态
    function batchCheckBlackPayers(address[] memory accounts) external view returns (bool[] memory) {
        bool[] memory results = new bool[](accounts.length);
        for (uint256 i = 0; i < accounts.length; i++) {
            results[i] = blackPayerList[accounts[i]].isBlacklisted;
        }
        return results;
    }
    
    // 批量检查多个地址的收款人黑名单状态
    function batchCheckBlackPayees(address[] memory accounts) external view returns (bool[] memory) {
        bool[] memory results = new bool[](accounts.length);
        for (uint256 i = 0; i < accounts.length; i++) {
            results[i] = blackPayeeList[accounts[i]].isBlacklisted;
        }
        return results;
    }
    
    // 批量获取付款人黑名单详细信息
    function batchGetBlackPayerInfo(address[] memory accounts) external view returns (
        bool[] memory isBlacklistedList,
        uint256[] memory addedTimeList,
        uint256[] memory removedTimeList
    ) {
        uint256 length = accounts.length;
        isBlacklistedList = new bool[](length);
        addedTimeList = new uint256[](length);
        removedTimeList = new uint256[](length);
        
        for (uint256 i = 0; i < length; i++) {
            BlackListInfo memory info = blackPayerList[accounts[i]];
            isBlacklistedList[i] = info.isBlacklisted;
            addedTimeList[i] = info.addedTime;
            removedTimeList[i] = info.removedTime;
        }
        
        return (isBlacklistedList, addedTimeList, removedTimeList);
    }
    
    // =============================================================
    // 通用批量更新功能(兼容您原来的需求)
    // =============================================================
    
    // 批量更新付款人黑名单(支持添加和移除)
    function updateBlackPayerList(address[] memory addresses, bool[] memory statuses) external onlyOwner {
        require(addresses.length == statuses.length, "Address and status arrays length mismatch");
        require(addresses.length > 0, "Address list cannot be empty");
        uint256 currentTime = block.timestamp;
        
        for (uint256 i = 0; i < addresses.length; i++) {
            address account = addresses[i];
            bool status = statuses[i];
            require(account != address(0), "Cannot update zero address");
            
            if (blackPayerList[account].isBlacklisted != status) {
                blackPayerList[account].isBlacklisted = status;
                
                if (status) {
                    blackPayerList[account].addedTime = currentTime;
                    emit BlackPayerAdded(account, currentTime);
                } else {
                    blackPayerList[account].removedTime = currentTime;
                    emit BlackPayerRemoved(account, currentTime);
                }
            }
        }
    }
    
    // 批量更新收款人黑名单(支持添加和移除)
    function updateBlackPayeeList(address[] memory addresses, bool[] memory statuses) external onlyOwner {
        require(addresses.length == statuses.length, "Address and status arrays length mismatch");
        require(addresses.length > 0, "Address list cannot be empty");
        uint256 currentTime = block.timestamp;
        
        for (uint256 i = 0; i < addresses.length; i++) {
            address account = addresses[i];
            bool status = statuses[i];
            require(account != address(0), "Cannot update zero address");
            
            if (blackPayeeList[account].isBlacklisted != status) {
                blackPayeeList[account].isBlacklisted = status;
                
                if (status) {
                    blackPayeeList[account].addedTime = currentTime;
                    emit BlackPayeeAdded(account, currentTime);
                } else {
                    blackPayeeList[account].removedTime = currentTime;
                    emit BlackPayeeRemoved(account, currentTime);
                }
            }
        }
    }
    
    // =============================================================
    // 时间相关的查询功能
    // =============================================================
    
    // 查询在指定时间段内被添加到付款人黑名单的地址数量
    // 注意:这个函数需要遍历所有地址,实际使用中可能需要使用事件日志来实现
    // 这里只是示例,实际实现可能需要离链查询事件
    function getBlackPayersAddedInTimeRange(uint256 /*startTime*/, uint256 /*endTime*/) external pure returns (uint256) {
        // 占位返回 - 实际实现需要事件日志查询
        return 0;
    }
    
    // 检查地址是否在指定时间被加入黑名单
    function wasBlacklistedAt(address account, uint256 timestamp, bool isPayer) external view returns (bool) {
        BlackListInfo memory info = isPayer ? blackPayerList[account] : blackPayeeList[account];
        
        if (info.addedTime == 0) {
            return false; // 从未被加入黑名单
        }
        
        if (info.addedTime > timestamp) {
            return false; // 在指定时间后才被加入
        }
        
        if (info.removedTime > 0 && info.removedTime <= timestamp) {
            return false; // 在指定时间前已被移除
        }
        
        return true; // 在指定时间确实在黑名单中
    }
    
    // =============================================================
    // 实用工具函数
    // =============================================================
    
    // 获取当前区块时间戳
    function getCurrentTimestamp() external view returns (uint256) {
        return block.timestamp;
    }
    
    // 将时间戳转换为可读日期(需要前端处理)
    function getReadableTime(uint256 timestamp) external pure returns (string memory) {
        if (timestamp == 0) {
            return "Never set";
        }
        // 返回时间戳,前端可以转换为可读格式
        return string(abi.encodePacked("Timestamp: ", uint2str(timestamp)));
    }
    
    // 数字转字符串的辅助函数
    function uint2str(uint256 _i) internal pure returns (string memory) {
        if (_i == 0) {
            return "0";
        }
        uint256 j = _i;
        uint256 len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint256 k = len;
        while (_i != 0) {
            k = k - 1;
            uint8 temp = (48 + uint8(_i - _i / 10 * 10));
            bytes1 b1 = bytes1(temp);
            bstr[k] = b1;
            _i /= 10;
        }
        return string(bstr);
    }
    
    // 获取合约基本信息
    function getContractInfo() external view returns (
        string memory tokenName,
        string memory tokenSymbol,
        uint8 tokenDecimals,
        uint256 tokenTotalSupply,
        address contractOwner
    ) {
        return (
            name(),
            symbol(),
            decimals(),
            totalSupply(),
            owner()
        );
    }
}

看了代码后,就可以解决前两个问题了

  • 黑名单保存在哪里?
    • 保存在区块链上。
  • 可以通过一个URL来配置黑名单吗?
    • 不能,智能合约无法直接发起HTTP请求来检查外部URL,因为区块链是封闭的环境。

关于黑名单在区块链保存在哪里?可以参考下图会清晰一些:

二、发布合约到测试链Sepolia

还是使用Remix来部署合约,操作可以参考上一篇。

部署这个合约需要0.0062个ETH,主链的话就需要15美金,上一篇介绍的一个最简单的ERC20合约只需要3美金,毕竟这个代码很复杂,15美金也是正常的。

合约地址是: https://sepolia.etherscan.io/address/0x244af7a9faf2e494dfee1717e08e0a32fdbc9934

代币名称:IOCV2BL ,发行总量10000个。

三、需要公开代码,方便在线调用函数

把合约代码公开后,就可以在合约地址 https://sepolia.etherscan.io/token/0x244af7a9faf2e494dfee1717e08e0a32fdbc9934#code 直接读取合约或者写合约了,读合约的主要操作就是查询某个地址是否是黑明单,写合约操作就是单次或者批量的增加收付款黑名单地址及移除收付款黑名单地址。

四、未增加黑名单时转账

这时解答了第三个问题

  • MetaMask这样的Web3 Wallet可以使用这种ERC20代币吗?
    • 可以

五、对收款人地址增加收款黑名单

检查一下是否维护了黑名单,确实已经是黑名单了。

六、给增加了收款黑名单的地址发起转账

可以发现交易失败,并且可以在区块链浏览器查看到这笔错误交易,错误信息也是在智能合约里定义的”Transfer failed: recipient address is blacklisted”

这时也解答了第四个问题

  • 在智能合约里配置因黑名单造成转账失败的错误提示,可以被查看吗?
    • 可以,都在记录在区块链上,使用区块链浏览器查看。

区块链地址: https://sepolia.etherscan.io/tx/0x85d370c9c9a0feba43930b28a507426728b40bf56adea127d776952050603e78

定义错误描述的代码部分见下:

Leo

[转载]从上帝视角看支付——支付总架构解析

从上帝视角看支付——支付总架构解析 | 人人都是产品经理编辑导语:随着互联网行业的不断发展,如今各行业都在朝着互联网方向发展,互联网支付变得非常方便,用户可以在电商平台进行直接购物以及支付;本文作者分享了关于支付总架构的解析,我们一起来了解一下。

编辑导语:随着互联网行业的不断发展,如今各行业都在朝着互联网方向发展,互联网支付变得非常方便,用户可以在电商平台进行直接购物以及支付;本文作者分享了关于支付总架构的解析,我们一起来了解一下。

中国支付体系的塔尖是人行二代支付清算体系,人行二代支付体系塔尖是清算账户中心 SAPS;支付的基础是账户,账户的账户是 SAPS。

为了更好的认认识支付——下面我们就从用户触发,跟着一笔支付遍历整个互联网支付网络,对支付有一个最大宏观视角的认知!

一、上帝视角

1. 看问题的最佳位置

研究人类的上帝视角位置我觉得是 DNA 双链,因为起源;研究支付的上帝视角位置,我觉得是这里!俯视整个互联网支付大厦!

二、互联网支付总架构

1. 看问题的宏观视野

我们对中国整个互联网支付按照参与者进行分层:

下面我们跟着一笔支付交易遨游这座大厦。

三、互联网用户

1. 好体验让用户更疯狂

我是京东商城忠实爱好者,曾今因为崇拜东哥的经营理念而加入京东;所以购物基本都是用京东,一个是对自营的信任,对物流和服务的认可,另一个就是京东极致的用户体验。

作为亿万互联网用户中的一员,我们都一样!为好的服务,好玩的产品买单。

那么既然要买单,就需要付钱咯;如今移动互联网时代以及互联网支付已经非常成熟,各种支付方式、支付应用琳琅满目;我们可以用微信,支付宝,银行卡,还可以免密支付。

这不,618 在京东用招商信用卡买了一本书……

收银台,我们支付的第一步!走起

四、第一层互联网服务平台

1. 好产品让生活更便捷

京东购物,美团点外卖,支付宝转账,去哪儿买机票去旅游,好产品让生活变得更便捷;那么我们称该层为互联网应用层,这一层为用户直接提供服务,和支付能力。

这些平台为用户提供各式各样的服务,是直接面向用户的互联网应用;用户在平台购买服务,平台就需要有自己的支付体系来支撑支付业务。

2. 服务平台的支付架构

刚才我们说从京东买了一本书,那么京东商城的支付架构是怎么样的呢!刚才我买书支付的那笔钱怎么流转呢?

图片来自网络

选好商品提交订单以后,平台在订单中心完成订单的创建,订单在交易中心完成账单的创建;我们操作去支付,交易请求支付获得收银台链接,给到订单,订单再将收银台链接返回给我们,这时候我选择了用招商信用卡支付,输入支付密码,一瞬间支付成功!

其实这几秒钟整个支付的链条跋山涉水,翻山越岭经历千险,

3. 支付架构解析

我们看上面的架构图,对于一个服务平台的支付架构,一般有图中的相关系统组成:直面用户的收银台,记录业务的订单系统,推动交易的交易系统,对支付指令进行处理的支付系统,支付指令传送通道的支付通道子系统。

另外支付成功后还有一条线清结算线:支付成功以后交易将数据提交清算中心完成数据的清分计算,然后提交账务系统完成记账;再通知会计核心完成内部账的记录;最后通知资金平台对交易向商家进行货款的结算……

这样对于一个服务平台来说,一个支付的骨架就出来了!

这个架子是笔者多年游历访学的精华沉淀;基本适用于美团,去哪儿,滴滴等,如果你要从零到一做一套支付体系,那么这么规划基本没什么问题。

好我们接着往下说,我们支付商家的支付都需要接入一个支付机构的,支付系统通过支付通道将支付请求提交给了签约的支付服务提供方,我们以三方支付为例,比如京东将支付提交给了网银在线!那么网银在线内部又是怎么处理的呢?

五、第二层三方支付体系

1. 好支付让交易更安全

支付机构作为拥有支付牌照,为服务平台提供支付解决方案的企业,也有着自己复杂而庞大的支付体系,我们常听说的比如各类收银台,支付产品,支付路由,支付通道,支付核心,账务核心,清算核心,风控核心,商户入网等等。

对于支付机构来说支付产品的创新满足支付市场需要是至关重要的,支付产品创新主要聚焦在下面几个维度:

2. 支付机构的支付架构

那么对于一个支付机构来说,他们的支付架构是什么样的呢?比如刚才支付请求到了支付机构,这笔支付在支付机构会怎么流转和处理呢?

我们在网上可以看到很多支付机构的架构图,上面这个稍微简单点架构图基本涵盖了一家支付机构应该具备的基础能力;比如交易层对交易请求进行处理;支付层对支付请求进行处理;渠道层对支付请求进行提交金融机构或者清算机构完成最后的清算请求指令的提交

既然都是支付,我们可以换个思路,其实支付机构的支付架子跟服务平台的架子在某些角度看大同小异;只不过是服务用户对象一个是用户一个是商户,支付通道一个是三方机构提供,一个是银行提供

3. 支付机构架构解析

好了,我们来看支付请求来到了三方支付机构之后….

第一个门槛就是到达支付机构的网关层,通过各种风控校验通过许可,来到开放平台,开放平台将支付请求提交给交易处理层进行处理;首先到达订单系统,创建订单后请求支付系统获得收银台链接,返回给开放平台;开放平台处理支付通过收银台请求支付系统进行支付处理……

支付处理中心对支付请求进行处理,通过路由选择合适的支付渠道,然后由渠道清算封装支付指令,通过支付通道提交清算指令给清算机构或者银行。

清算机构返回清算成功后,支付处理中心通知订单中心支付成功,订单中心将支付单提交给清算中心进行清结算处理,完成计费,账务记账,入账等处理操作。

这样我们整个架子就出来了:

如果你不小心捡了一个支付牌照,那么拿着陈老师的图,去搭建你的支付公司吧;支付机构将支付清算指令提交给了清算机构,因为现在断直连了嘛,不能直接接入银行了;断直连后其实银联网联网关于支付清算的处理是一样的,那么我们以网联为例,看看支付指令到了网联之后会怎么样的呢!

六、第三层网银联清算体系

1. 好清算让市场更和谐

笔者有幸参与了支付机构断直连接入网联的盛世,参与了很多场网联的会议,惊叹于断直连后的清算架构!!!

我们先看这张图,从宏观上认知这几个角色之间的关系:

为解决备付金集中存管所形成的热点账户问题,实现对已映射额度管理,网联将构建 “备付金热点账户前置系统”RCMP,用于支付机构通过网联平台(EPCC)的业务办理;前置系统分为额度管理模块及账户管理模块,网联将为各支付机构在前置系统中建立账户,用于可用额度的监控、已映射额度的管理。

支付机构的指令到了网联以后,网联进行实时清算,什么意思呢?就是实时的对支付指令进行轧差变更可用余额;针对网联的文章我们说的比较详细,这里就不过多介绍了,简单的说就是支付机构将人行备付金的余额映射分配给网联和银联形成映射虚拟额度,用于交易周期内的实时清算;然后定时提交人行进行资金的划拨结算,这个框架是这样的:

2. 清算概念

其实断直连以后网银联的清算变得大道至简了很多,当然说到清算我们还是有必要说一下银联这个老牌清算机构的清算架构的,再说清算之前,我们先开个小灶,介绍下清结算相关的之歌比较重要的概念。

支付结算是指单位、个人在社会经济活动中使用票据、信用卡和汇兑、托收承付、委托收款等结算方式进行货币给付及其资金清算的行为。银行是支付结算和资金清算的中介机构。

支付清算是指支付指令的交换和计算;支付指令是指参与者以纸质、磁介质或电子形式发出的,办理确定金额的资金转账命令; 支付指令的交换是指提供专用的支付指令传输路径,用于支付指令的接收、清分和发送;支付指令的计算是指对支付指令进行汇总和轧差;参与者是指接受支付清算组织章程制约,可以发送、接收支付指令的金融机构及其他机构。

银联的支付清算包括淸分和资金划拨两个环节。淸分是指对交易日志中记录的成功交易,逐笔计算交易本金及交易费用(手续费、分润等),然后按清算对象汇总轧差形成应收或应付金额。

简言之,就是搞清楚今天应该向谁要多少钱?应该给谁多少钱?资金划拨是指通过特定的渠道和方式,完成应收应付资金的转移;简言之,就是明确通过何种渠道,拿回应收款、付出应付款。

3. 银联的清算架构

银联的支付清算包括跨行清算和收单清算——跨行清算是针对收单机构和发卡机构的清算;收单清算是代替收单机构针对商户和收单专业化服务机构的清算。

银联清算系统主要就是三个核心:跨行清算子系统,收单清算子系统,资金管理平台。

4. 银联的清算关系

清算离不开一个核心,那就是清算账户,所有的支付以及清算都是基于账户进行账务处理;清算账户与结算账户不是同一概念,两者的区别源于【清算】 与【结算】的区别。

银联境内清算的清算账户均开立在人民银行,跨境业务的清算账户开立在代理清算银行(中行和汇丰),境内成员机构的清算账户均开立在人民银行;银行一般在人民银行开立有准备金账户,一般使用其备付金账户用于和银联的清算。

境内商户的结算账户开在商业银行,第三方机构的结算账户均开立在人民银行。

银联如何进行资金的划拨呢;境内的跨行清算通过央行的大额支付清算系统,完成资金划拨,银联可以主动借记或贷记成员机构的清算账户账户;通俗地讲,借记就是我问别人要钱,贷记就是我给别人钱。境内的收单清算可以通过央行的小额支付清算系统完成资金划拨。

银联清算系统与大小额支付清算系统的关系,无论是跨行清算还是收单清算,银联都是作为一个特许参与者,加入大小额支付清算系统,完成银行卡交换业务的资金划拨。

银联通过大额支付系统,实现与境内成员机构清算账户之间的双向资金转移;银联通过小额支付系统和当地的票据交换系统,实现与境内第三方机构和商户之间的单向资金转移。

银联清算系统与银行结算系统的关系,银联和商业银行都是作为参与者,加入大小额支付清算系统,完成跨行间的资金划拨;银联清算系统的清算对象是成员机构、第三方机构和直联商户。商业银行结算系统的结算对象是在其本行开立存款账户的单位或个人。

银联在央行开立的清算账户从本质上说应属于备付金账户;而商业银行在央行开立的清算账户分准备金账户和备付金账户。准备金账户主要用于监管使用,用于包括存款人合法权益;备付金账户主要用于自身的资金头寸的管理。

银联清算系统与银联会计核算系统的关系,银联清算系统处理的是银行卡交换的清算资金。银联会计核算系统处理的是银联的自有资金,其中自有资金中包括了银联自己清算账户上的资金余额。

银联会计核算系统是按照企业会计准则,使用总分户账,登记账户变动及资金转移信息。银联清算系统仅建立了清算资金的台账信息。

5. 清算案例

我们看一个清算案例:张三 6 月 1 日持招行贷记卡,在物美超市(直联商户,工行收单)成功刷卡购物 1000 元。李四用工行借记卡,在华夏银行布放的支付易终端上,成功缴付了一笔 200 元的电费,华夏银行收单。

假设消费交易执行交换费 0.7%,转接费 0.1%;缴费交易执行交换费 0.10 元,转接费 0.05 元。收单行与商户的扣率实际分别为 1% 和 0.08%(这两个数字没用到),简化起见不计算收单专业化服务机构的手续费。

淸分结果:

1)6 月 1 日晚 11:00CUPS 日切。将交易日志发送跨行清算系统。在 6 月 2 日凌晨首先进行跨行转接交易的淸分,然后进行收单处理业务的淸分

2)6 月 2 日上午 10:00 左右,将汇总的淸分结果,通过资金管理平台和支付前置系统发出支付指令。先借记后贷记,按优先级排队。

3)通过大额系统借记银行在央行开立的备付金账户,实时完成跨行清算的资金转移(借记招行 993 元、借记工行 197.9 元);通过小额系统贷记商户开立在银行(一般为工行)的结算账户(990 元)(6 月 2 日中午左右到账);通过大额贷记银行在央行开立的备付金账户,实时完成跨行及收单清算(贷记华夏 199.85 元)。

4)华夏银行收到银联划付的资金后,通过本行的行内结算系统,贷记间联商户的结算账户(199.84 元,华夏收单收益 0.01 元,约 6 月 3 日到账);工行和招行或调整持卡人的可用余额(联机交易时银行已实时扣减了持卡人的账户余额或可用额度)。

6. 通用清算机构架构

那么一个清算机构,会拥有什么样的架构呢,这个更细维度的架构大家作为了解,上面的宏观维度的认知已经足够了;虽然社群后不少来自网联银联的朋友,但我相信大部分同学是不会去这样的机构的,所以一眼带过就可以了。

我们知道网联银联将支付机构的结算指令提交人行进行最终的资金划拨,完成各参与主体之间的资金实际交割;那么在将人行之前,我们先看看接入网联银联的商业银行的支付架构是什么样的呢?

七、第四层银行金融体系

1. 好金融让支付更丰富

银行相当于服务平台,三方支付机构以及网联这样的清算机构是有很大不同的,首先是业务的不同,银行处理提供互联网支付通道以外,还有门店,ATM,银行卡,存款业务,贷款业务,理财业务等等。

我们简化一下:

抛开繁多的银行其他业务,我们来看银行跟互联网支付相关的业务架构是怎么样呢,不用说我相信大家也猜到了,肯定会有账户,有支付核心,有通道,有前置系统。

我们再看下更宏观的一个,像图中的其他渠道我们可以理解为提供给网联银联或者支付公司的支付通道,接入银行进行支付请求,然后银行请求人行。

好了,下面我们要去朝圣了,到达支付的顶层设计 – 人民银行二代支付系统!

八、第五层人行支付体系

1. 好基础让支付更

我们都知道中国支付系统经历了支付一代系统和支付二代系统;为整个中国互联网支付提供清算基础设施;那么这个神秘的支付体系拥有如何神奇的架构呢,在开始之间我们来看一个非常经典的架构图:中国清算系统。

最组边的就是网联银联以及三方支付机构所处的位置,上面的就是我们要讲的人行部分,下面是商业银行所处的位置;其中网联银联和商业银行直联央行的支付清算系统。

我们来看整体的业务架子就是下面这张图,各参与者连接到 NPC 就是国家处理中心,包含了大小额支付系统和网银互联跨行支付系统(超级网银),然后 NPC 经过支付指令的处理之后提交清算账户中心(SAPS)进行资金的清算划拨。

2. 几个核心系统

我们来看二代支付系统最核心的几个系统,大家从我的支付体系全解析目录中也看到了,人行部分会对每个系统做详细介绍,这里我们就一带而过,主要是知道一个大概。

以清算账户管理系统为核心,大额支付系统、小额支付系统、支票影像交换系统、网银互联子系统为业务应用子系统,公共管理控制系统和支付管理信息系统为支持系统。

我们来认识一下这几个系统:

清算账户管理系统(SAPS):是支付系统的核心系统,通过集中存储和管理清算账户,完成支付系统各类业务的资金清算,并为中央银行办理现金存取、再贷款、再贴现等业务提供清算服务;各银行,支付机构,网联银联都会在这里开通清算账户,进行资金的清算。

大额支付系统,小额支付系统,网上支付跨行清算系统就是我们所理解的支付系统,接受参与者的清算支付指令,进行指令的处理并提交给清算账户管理系统完成资金划拨。

支付信息管理系统:是中国现代化支付系统的辅助支持系统,由行名行号管理子系统、参数管理子系统、计费管理子系统、支付业务统计分析子系统、支付业务监控子系统等六个子系统组成,是一个多功能模块的、集中式的支付信息共享平台。

3. 系统的运行控制

各系统的运行时序图:

我们来看一下各系统之间的运行是如何进行控制的:

4. 支付清算处理

然后我们来看网联将结算请求提交给人行支付系统之后,支付系统对指令进行处理之后提交清算账户系统完成最终的资金划拨;也就是这个时候陈老师买书的钱才真正从招商的清算账户进入到网银在线的备付金账户中。

到这里我们就将整个支付大厦的每一层参与者的架构都做了一个宏观的认识,现在你是不是已经知道了,一笔支付要经历多么漫长的链条了吧。

最后我们将所有的参与者架构拼接到一起,来看这座美丽奢华的支付大厦。

九、遨游支付

我们将所有的支付参与者,也就是每一层的职能和架构都做了介绍,那么跟着我来遨游一下一笔支付的神奇之旅吧!

以后的文章会逐步深入了解每一个模块每一个系统的详细设计讲解。

作者:陈晓光;微信公众号:陈晓光;10 年产品设计经验,4 年社交,2 年电商,5 年支付;曾任职于某头部金融,某头部支付机构;云对账创始人获千万融资。

Leo

XAMPP-VM for macOS

XAMPP-VM版是在MAC配置了一个虚拟机的方式,虚拟机的路径:

~/.bitnami/stackman/machines/xampp/lampp

所以手动卸载的话也需要手动删除“~/.bitnami/”路径。

Leo

添加网易云音乐播放组件不能自动播放的解决办法?

添加了网易云音乐的播放代码后,有时候不能自动播放,网易云音乐官方的引用代码如下:

<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=190 height=86 src="//music.163.com/outchain/player?type=2&id=473115525&auto=1&height=66"></iframe>

不能自动播放时控制台会提示:

Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first. https://goo.gl/xX8pDD

看看 https://goo.gl/xX8pDD ,大概意思就是说为了避免网页音乐给用户带来困恼,Chrome从Version 66后增加了控制:

Chrome浏览器的自动播放政策将于2018年4月更改。

网易云音乐是用iframe来嵌入的,看看“顶部框架可以将自动播放权限委派给其iframe,以允许自动播放声音。”这个能否解决。

官方介绍了解决方法:

<!-- Autoplay is allowed. -->
<iframe src="https://cross-origin.com/myvideo.html" allow="autoplay">

<!-- Autoplay and Fullscreen are allowed. -->
<iframe src="https://cross-origin.com/myvideo.html" allow="autoplay; fullscreen">

改一下原来的网易云音乐引用代码:

<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=190 height=86 src="//music.163.com/outchain/player?type=2&id=473115525&auto=1&height=66" allow="autoplay"></iframe>

再尝试刷新几次首页,发现可以自动播放了。(注:也并不是每次都可以,但是感觉概率比之前大了)

Leo

有趣的Base58

大多数人都熟悉的是Base64,不太了解Base58,Base58我认为是懂客户体验的程序员设计出来的。

先聊下Base64,Base64是用64个字符表示二进制数据的表示方法。

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Base58是用于Bitcoin中使用的一种独特的编码方式,主要用于产生Bitcoin的钱包地址。相比Base64,Base58不使用数字”0″,字母大写”O”,字母大写”I”,和字母小写”l”,以及”+”和”/”符号。

https://zh.wikipedia.org/wiki/Base58

大家都在讲客户体验,客户体验不仅仅是产品经理的活儿,Base58可以算是懂客户体验的程序员设计出来的:

  1. 避免混淆。在某些字体下,数字0和字母大写O,以及字母大写I和字母小写l会非常相似。
  2. 不使用”+”和”/”的原因是非字母或数字的字符串作为帐号较难被接受。
  3. 没有标点符号,通常不会被从中间分行。
  4. 大部分的软件支持双击选择整个字符串。

I'M LIANG DONG

探索

梁棟Leo Liang我的名片
90 条动态2 条回应
登录后发动态

按分类浏览

按月份找回忆
邮件联系

外观

PERSONAL CARD / 個人名片

梁棟

Leo Liang

高級創新經理

創新優化中心

中國銀行(香港)有限公司

个人名片二维码扫码交换名片
liangdong.me

复制与分享

长按或选中下方内容进行复制。

搜索回忆

输入关键词,找回某个瞬间。