Home  >  Article  >  Backend Development  >  PHP payment system design and typical case sharing_php examples

PHP payment system design and typical case sharing_php examples

PHP中文网
PHP中文网Original
2016-08-17 13:02:33818browse

Due to the company's business needs, it took two weeks to implement a small payment system. Although it is small, it has all the essentials. Various necessary modules such as account locking, transaction guarantees, and accounting reconciliations are fully implemented. The entire payment system is fully implemented. I have accumulated a lot of experience during the development process, and after searching on the Internet, most of them are research papers with little practical value, so I specially share them with you this time.
This system can be used as a small payment system or as a payment flow system when third-party applications are connected to the open platform.
The original demand is more responsible, let me simplify it a bit:

For each application, external interfaces need to be provided to obtain balance, pay equipment, recharge and other interfaces
There are programs in the background, and liquidation will be carried out on the first of every month
Accounts can Being frozen
needs to record the flow of each operation, and the daily flow must be reconciled with the initiator

In response to the above requirements, we set up the following database:

CREATE TABLE `app_margin`.`tb_status` ( 
`appid` int(10) UNSIGNED NOT NULL, 
`freeze` int(10) NOT NULL DEFAULT 0, 
`create_time` datetime NOT NULL, 
`change_time` datetime NOT NULL, 
  
PRIMARY KEY (`appid`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  
CREATE TABLE `app_margin`.`tb_account_earn` ( 
`appid` int(10) UNSIGNED NOT NULL, 
`create_time` datetime NOT NULL, 
`balance` bigint(20) NOT NULL, 
`change_time` datetime NOT NULL, 
`seqid` int(10) NOT NULL DEFAULT 500000000, 
  
PRIMARY KEY (`appid`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  
CREATE TABLE `app_margin`.`tb_bill` ( 
`id` int AUTO_INCREMENT NOT NULL, 
`bill_id` int(10) NOT NULL, 
`amt` bigint(20) NOT NULL, 
`bill_info` text, 
  
`bill_user` char(128), 
`bill_time` datetime NOT NULL, 
`bill_type` int(10) NOT NULL, 
`bill_channel` int(10) NOT NULL, 
`bill_ret` int(10) NOT NULL, 
  
`appid` int(10) UNSIGNED NOT NULL, 
`old_balance` bigint(20) NOT NULL, 
`price_info` text, 
  
`src_ip` char(128), 
  
PRIMARY KEY (`id`), 
UNIQUE KEY `unique_bill` (`bill_id`,`bill_channel`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  
CREATE TABLE `app_margin`.`tb_assign` ( 
`id` int AUTO_INCREMENT NOT NULL, 
`assign_time` datetime NOT NULL, 
  
PRIMARY KEY (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  
CREATE TABLE `app_margin`.`tb_price` ( 
`name` char(128) NOT NULL, 
`price` int(10) NOT NULL, 
`info` text NOT NULL, 
  
PRIMARY KEY (`name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  
CREATE TABLE `app_margin`.`tb_applock` ( 
`appid` int(10) UNSIGNED NOT NULL, 
`lock_mode` int(10) NOT NULL DEFAULT 0, 
`change_time` datetime NOT NULL, 
  
PRIMARY KEY (`appid`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  
INSERT `app_margin`.`tb_assign` (`id`,`assign_time`) VALUES (100000000,now());

The detailed explanation is as follows:
tb_status Application status table. Responsible for whether the account is frozen and what type of account it is (the real requirement is that the application may have two accounts, so it is not listed here for simplicity)
appid application id
freeze whether to freeze
create_time creation time
change_time last modification time
tb_account_earn application's account balance table
appid application id
balance balance (unit is cent, do not use decimals to store, because decimals themselves are not accurate; in addition, php must be in a 64-bit machine to support bigint)
create_time creation time
change_time last modified Time
seqid Operation sequence number (to prevent concurrency, each update will +1)
tb_assign The table that assigns the pipeline id. The bill_id of tb_bill must be assigned by tb_assign
id Auto-increment id
create_time The creation time
tb_bill pipeline table. Responsible for recording each operation flow. The bill_id here is not the primary key, because the same bill_id may have two flows: payment and rollback.
id is an auto-incremented serial number.
bill_id is the serial number.
amt is the amount of the operation (this needs to be distinguished between positive and negative) , mainly to directly calculate the change in amount during a certain period of time when selecting all)
bill_info detailed information of the operation, such as 3 webservers, 2 db
bill_user operation user
bill_time billing time
bill_type billing type, the difference is to add Money or loss
bill_channel The source of the transaction, such as recharge, payment, rollback, settlement or others
bill_ret The return code of the transaction, including unprocessed, successful, and failed. The logic here will be explained later
appid application id
old_balance operation occurs The previous account balance
price_info records the unit price of the paid item when the recording operation occurs
src_ip client ip
tb_price unit price table, records the unit price of the machine
name machine unique identifier
price price
info description
tb_applock lock table, this is Designed to avoid concurrent write operations to an application, the specific code will be shown later
appid application id
lock_mode lock status. If it is 0, it is locked, if it is 1, it is locked.
change_time Last modified time
OK, after the library table is designed, let’s take a look at the most typical operations.

1. Payment operation
I’m here I only listed the way I currently implement it. It may not be the best, but it should be the most economical and meet the needs.
Let’s talk about the caller first. The logic is as follows:

PHP payment system design and typical case sharing_php examples

Then the corresponding internal logic of the payment system is as follows (only payment operations are listed, the rollback logic is similar, and the flow check is to check whether the corresponding payment flow exists):

PHP payment system design and typical case sharing_php examples

Commonly used error return codes may be as follows:

$g_site_error = array( 
-1 => '服务器繁忙', 
-2 => '数据库读取错误', 
-3 => '数据库写入错误', 
  
0 => '成功', 
  
1 => '没有数据', 
2 => '没有权限', 
3 => '余额不足', 
4 => '账户被冻结', 
5 => '账户被锁定', 
6 => '参数错误', 
);

Errors greater than 0 are considered logical errors. When performing payment operations, the caller does not need to record the transaction. Because the account has not changed.
Errors less than 0 are internal system errors. Because it is not known whether data changes have occurred, both the caller and the payment system must record the flow.
For a return equal to 0, it means success, and both sides must also record the flow.
In the payment system, there is a reason why the transaction is written first and then the account is updated. Simply put, it is to try to avoid losing the transaction.
Finally, to summarize, this method of deducting money first, then shipping, and then rolling back if there is a problem is one model; another is to withhold first, then ship, and if there is no problem, call the payment confirmation to deduct the money. If something goes wrong, call payment rollback to cancel it. If no confirmation is made for a long time after withholding, the amount will be automatically rolled back.

2. Implementation of account locking
The locking mechanism of the database is used here. The specific logic will not be mentioned. The code is as follows:

class AppLock 
{ 
function __construct($appid) 
{ 
$this->m_appid = $appid; 
//初始化数据 
$this->get(); 
} 
  
function __destruct() 
{ 
$this->free(); 
} 
  
  
public function alloc() 
{ 
if ($this->m_bGot == true) 
{ 
return true; 
} 
  
$this->repairData(); 
  
$appid = $this->m_appid; 
$ret = $this->update($appid,APPLOCK_MODE_FREE,APPLOCK_MODE_ALLOC); 
if ($ret === false) 
{ 
app_error_log("applock alloc fail"); 
return false; 
} 
if ($ret m_bGot = true; 
return true; 
} 
  
public function free() 
{ 
if ($this->m_bGot != true) 
{ 
return true; 
} 
  
$appid = $this->m_appid; 
$ret = $this->update($appid,APPLOCK_MODE_ALLOC,APPLOCK_MODE_FREE); 
if ($ret === false) 
{ 
app_error_log("applock free fail"); 
return false; 
} 
if ($ret m_bGot = false; 
return true; 
} 
  
function repairData() 
{ 
$db = APP_DB(); 
  
$appid = $this->m_appid; 
  
$now = time(); 
  
$need_time = $now - APPLOCK_REPAIR_SECS; 
  
$str_need_time = date("Y-m-d H:i:s", $need_time); 
  
$db->where("appid",$appid); 
$db->where("lock_mode",APPLOCK_MODE_ALLOC); 
$db->where("change_time set("lock_mode",APPLOCK_MODE_FREE); 
$db->set("change_time","NOW()",false); 
  
$ret = $db->update(TB_APPLOCK); 
if ($ret === false) 
{ 
app_error_log("repair applock error,appid:$appid"); 
return false; 
} 
return true; 
} 
  
private function get() 
{ 
$db = APP_DB(); 
  
$appid = $this->m_appid; 
  
$db->where('appid', $appid); 
  
$query = $db->get(TB_APPLOCK); 
  
if ($query === false) 
{ 
app_error_log("AppLock get fail.appid:$appid"); 
return false; 
} 
  
if (count($query->result_array()) $appid, 
'lock_mode'=>APPLOCK_MODE_FREE, 
); 
$db->set('change_time','NOW()',false); 
$ret = $db->insert(TB_APPLOCK, $applock_data); 
if ($ret === false) 
{ 
app_error_log("applock insert fail:$appid"); 
return false; 
} 
  
//重新获取数据 
$db->where('appid', $appid); 
$query = $db->get(TB_APPLOCK); 
  
if ($query === false) 
{ 
app_error_log("AppLock get fail.appid:$appid"); 
return false; 
} 
if (count($query->result_array()) row_array(); 
return $applock_data; 
} 
  
private function update($appid,$old_lock_mode,$new_lock_mode) 
{ 
$db = APP_DB(); 
  
$db->where('appid',$appid); 
$db->where('lock_mode',$old_lock_mode); 
  
$db->set('lock_mode',$new_lock_mode); 
$db->set('change_time','NOW()',false); 
  
$ret = $db->update(TB_APPLOCK); 
if ($ret === false) 
{ 
app_error_log("update applock error,appid:$appid,old_lock_mode:$old_lock_mode,new_lock_mode:$new_lock_mode"); 
return false; 
} 
return $db->affected_rows(); 
} 
  
//是否获取到了锁 
public $m_bGot = false; 
  
public $m_appid; 
}

In order to prevent the problem of deadlock, the timeout judgment is added to the logic of acquiring the lock. You should be able to understand it by looking at the code

3. Reconciliation logic
If it is designed according to the above system, then reconciliation When doing so, you only need to check the successful transactions (i.e. bill_ret=0) on both sides. If they are completely consistent, there should be no problem with the account. If they are inconsistent, then you need to check the problem.
Regarding ensuring the correctness of the account, a colleague also told me that when I was working in the company, before any write operation, all the transaction records in the transaction table were taken first, and the values ​​​​of amt were accumulated and visible. Is the result the same as the balance? If they are not the same, there should be something wrong.
select sum(amt) from tb_bill where appid=1;
So this is why in my flow table, the amt field must distinguish between positive and negative.

The above is the entire content of this article. I hope it will be helpful to everyone’s study. For more related content, please pay attention to the PHP Chinese website (www.php.cn)!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn