PHP payment system design and typical case sharing _php example

Source: Internet
Author: User
Tags datetime php payment system rollback

With the company's business needs, it took two weeks to implement a small payment system, though small spite, all kinds of necessary modules such as account lock, transactional assurance, running water reconciliation are fully realized, the whole development process has a lot of experience accumulation, plus online search for a while, most of them are research papers, The actual use of the value is not very small, so this is deliberately taken out and share with you.
The system can be used as a small payment system or as a payment flow system for third party applications when accessing an open platform.
The original demand is more responsible, I simplified a little said:

For each application, external need to provide access to balance, payment equipment, recharge and other interfaces
The backstage has the procedure, the monthly number carries on the liquidation
Accounts can be frozen
Need to record every operation of the water, the daily water must be reconciled with the initiator

For the above requirements, we set up the following database:

CREATE TABLE ' app_margin '. ' Tb_status ' (' AppID ' int () UNSIGNED not NULL, ' freeze ' int (a) not null DEFAULT 0, ' creat E_time ' datetime NOT NULL, ' change_time ' datetime NOT NULL, PRIMARY KEY (' AppID ') Engine=innodb DEFAULT charset=ut 
  
F8; CREATE TABLE ' app_margin '. ' Tb_account_earn ' (' AppID ' int () UNSIGNED not null, ' create_time ' datetime NOT NULL, ' Bala NCE ' bigint not null, ' change_time ' datetime NOT NULL, ' seqid ' int (a) 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 (a) NOT NULL, ' Amt ' bigint NO T null, ' bill_info ' text, ' Bill_user ' char (128), ' bill_time ' datetime NOT NULL, ' Bill_type ' int (a) not null, ' BIL L_channel ' int ' not NULL, ' Bill_ret ' int (a) not null, ' AppID ' int (a) 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 K 
  
EY (' id ')) engine=innodb DEFAULT Charset=utf8; CREATE TABLE ' app_margin '. ' Tb_price ' (' Name ' char (128) NOT NULL, ' price ' int (a) not null, ' info ' text NOT NULL, P 
  
Rimary KEY (' name ')) Engine=innodb DEFAULT Charset=utf8; CREATE TABLE ' app_margin '. ' Tb_applock ' (' AppID ' int () UNSIGNED not null, ' Lock_mode ' int (a) not null DEFAULT 0, ' ch 
  
Ange_time ' datetime not NULL, PRIMARY KEY (' AppID ')) Engine=innodb DEFAULT Charset=utf8; INSERT ' App_margin '. ' Tb_assign ' (' id ', ' assign_time ') VALUES (100000000,now ());

Detailed explanations are as follows:
Tb_status the status table applied. Responsible for the account is frozen, what type of account (the real demand is that the application may have two types of accounts, here for the simple so not listed)
AppID Application ID
Whether the freeze is frozen
Create_time creation Time
Change_time Last Modification time
Account balance sheet for Tb_account_earn application
AppID Application ID
Balance balance (unit is divided, do not use decimal storage, because the decimal itself is not accurate; In addition, PHP to 64-bit machine to support bigint)
Create_time creation Time
Change_time Last Modification time
Seqid Operation serial Number (anti concurrency, every update will be + 1)
Tb_assign allocation of Water ID table, Tb_bill bill_id must be tb_assign allocated
ID self-Increasing ID
Create_time creation Time
Tb_bill water table. Responsible for recording each operation of the running water, where the bill_id is not a primary key, because the same bill_id may have to pay and roll back two of streams
ID self-increasing serial number
bill_id Serial Number
The amount of AMT operation (this is to distinguish between positive and negative, mainly for select all when you can directly calculate the amount of time changes)
Details of bill_info operations, such as 3 sets of webserver,2 db
Bill_user Operation user
Bill_time Running time
Bill_type flow type, to distinguish whether to add money or reduce money
Bill_channel flow sources, such as recharge, pay, rollback, settlement or other
Bill_ret The return code of the stream, including unhandled, successful, and failed, the logic here will be explained later
AppID Application ID
Account balance prior to old_balance operation
Price_info Record the unit price of the item being paid when the record operation occurs
SRC_IP Client IP
Tb_price Unit Price list, record the unit price of the machine
Name Machine Unique identification
Price prices
Info description
Tb_applock locks the table, which is designed to avoid concurrent write operations on an application, and the specific code is shown later
AppID Application ID
Lock_mode lock status. 0 is locked, 1 is locked
Change_time Last Modification time
OK, after the library table is designed, let's take a look at some of the most typical operations.

I. Payment operations
I've only listed the way I'm currently implementing it, probably not the best, but it should be the most economical and satisfying demand.
First the caller here, the logic is as follows:

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

The common error-return code may be sufficient:

$g _site_error = Array ( 
-1 => ' server busy ', 
-2 => ' database read error ', 
-3 => ' database write error ', 
  
0 => ' success ', 
  
1 => ; ' No data ', 
2 => ' no Permissions ', 
3 => ' balances not Enough ', 
4 => ' account Frozen ', 
5 => ' account Locked ', 
6 => ' parameter error ', 
);

For errors greater than 0, it is a logical error to perform a payment operation, and the caller does not have to record the running water. Because the account has not changed anything.
An error less than 0 is an internal system error because the caller and the payment system have to log running water because they do not know if data changes have occurred.
For the return equal to 0, the representative succeeds, both sides also must record the water.
And in the payment system, the reason to use the first write water, and then update the account of the way is also a reason, simply to avoid the loss of water.
Finally summed up, the first deduction of money, then shipped, the problem and then roll back the way is a model; there is a first button, after delivery, no problem is called payment confirmation to deduct, out of the question on the call to pay rollback to cancel, if the withholding after a long time does not make any confirmation, then the amount will be automatically rolled back.

Two. Account Lockout implementation
This uses the lock mechanism of the database, the concrete logic does not say, the code is as follows:

Class Applock {function __construct ($appid) {$this->m_appid = $appid; 
Initialization Data $this->get (); 
function __destruct () {$this->free (); 
The 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 <= 0) {app_error_log ("Applock alloc fail,affected_rows: $ret"); 
return false; 
} $this->m_bgot = true; 
return true; 
The 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 <= 0) {app_error_log ("Applock free Fail,affected_rows: $ret"); 
return false; 
} $this->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 <=", $str _need_time); 
$db->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; The If (Count ($query->result_array ()) <= 0) {$applock _data = array (' AppID ' => $appid, ' Lock_mode ' =>applo 
Ck_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; 
//re-acquire Data $db->where (' AppID ', $appid); 
  
$query = $db->get (tb_applock); 
if ($query = = False) {App_error_log ("Applock get Fail.appid: $appid"); 
return false; 
The If (Count ($query->result_array ()) <= 0) {app_error_log ("Applock not data,appid: $appid"); 
return false; 
}} $applock _data = $query->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 (); //Whether the lock was acquired 
Public $m _bgot = false; 
Public $m _appid;
 }

In order to prevent the deadlock problem, the logic of acquiring the lock has added the judgment of timeout time, we can see the code should be able to understand

Three. Reconciliation logic
If you follow the above system to design, then the account, as long as the two sides of the success (that is, bill_ret=0) of the water can, if the full agreement then the account should be no problem, if not consistent, it will be to check the problem.
On the guarantee of the correctness of the account here, there are colleagues to say to me, before the company to do, is to take as long as there is any write operation before, all the water table to take all the records, the Amt of the value of the sum up to see whether the results are the same as the balance. If it's not the same, it should be a problem.
Select SUM (AMT) from Tb_bill where appid=1;
So that's why I'm in the flow table, the Amt field is to distinguish between positive and negative reasons.

The above is the entire content of this article, I hope to help you learn, but also hope that we support the cloud habitat community.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.