PHP payment system design and typical case sharing _ php instance

Source: Internet
Author: User
Tags php payment system
This article mainly introduces the PHP payment system design and typical cases, which can be used as a small payment system or a payment flow system when third-party applications access the open platform, it has some reference value. if you are interested, you can refer to a small payment system that took two weeks to implement because of the company's business needs, all necessary modules, such as account locking, transaction guarantee, and flow reconciliation, are fully implemented. there is a lot of experience accumulated throughout the development process, and I searched for them online, most of them are research papers, which are of little practical value. Therefore, I would like to share with you this time.
This system can be used as a small payment system or as a payment flow system when third-party applications access the open platform.
The original requirements are relatively responsible. I would like to simplify it:

For each application, you need to provide external interfaces such as obtaining balance, payment devices, and recharging.
There are programs in the background, and liquidation is performed on the first day of every month.
Account can be frozen
The flow of each operation needs to be recorded, and the daily flow must be recorded with the initiator for reconciliation.

For the above requirements, we set 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:
Status Table of the tb_status application. Whether the responsible account is frozen or not, and what type of account is (the actual requirement is that there may be two types of accounts for the application, which is simple and not listed here)
Appid application id
Freeze?
Create_time creation time
Change_time last modification time
Account balance table of the tb_account_earn application
Appid application id
Balance (unit: Minute. do not use decimals for storage, because decimals are not precise. In addition, php must support bigint on a 64-bit host)
Create_time creation time
Change_time last modification time
Seqid operation serial number (anti-concurrency, + 1 each update)
For tables where tb_assign assigns a flow id, the bill_id of tb_bill must be allocated by tb_assign.
Id auto-increment id
Create_time creation time
Tb_bill flow meter. Records each operation flow. the bill_id here is not the primary key, because the same bill_id may have two flow records: payment and rollback.
Id auto-incrementing serial number
Bill_id serial number
Amt operation amount (this is to distinguish between positive and negative values, mainly to calculate the amount change for a certain period of time when selecting all)
Bill_info operation details, such as 3 webservers and 2 databases
Bill_user
Bill_time
Bill_type indicates whether to add or deduct money.
Bill_channel flow source, such as recharge, payment, rollback, settlement or other
The return code of bill_ret flow, including unprocessed, successful, and failed. the logic here will be explained later.
Appid application id
Account balance before the old_balance operation
Price_info records the unit price of the paid item when an operation occurs.
Src_ip client ip
The tb_price unit price table records the unit price of the machine.
Unique name of the machine
Price
Info description
The tb_applock lock table is designed to avoid concurrent write operations on an application. the specific code will be displayed 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 modification time
OK. after the database and table are designed, let's take a look at the most typical operations.

I. payment operations
I only list the current implementation methods, which may not be the best, but should be the most economical and satisfying.
Let's talk about the caller here. the logic is as follows:

$ G_site_error = array (-1 => 'Server busy ',-2 => 'Database read error',-3 => 'Database write error ', 0 => 'success', 1 => 'No data', 2 => 'No authorization', 3 => 'insufficient balance ', 4 => 'account frozen ', 5 => 'account locked ', 6 => 'parameter error ',);

Errors greater than 0 are regarded as logical errors. if you perform the payment operation, the caller does not need to record the flow. Because the account has not changed.
For errors smaller than 0, it is an internal system error. because you do not know whether data changes have occurred, both the caller and the payment system must record the flow.
If the return value is equal to 0, it indicates success, and the two sides must also record the flow.
In the payment system, there is also a reason for the first write flow and then account update. Simply put, we try to avoid the loss of flow.
Finally, we can conclude that the method of first deduction, then delivery, and then rollback due to problems is one mode; the other is pre-deduction, and then delivery, if there is no problem, call payment confirmation to deduct the fee. if a problem occurs, call payment rollback to cancel the payment. If no confirmation is made for a long time after the pre-deduction, the amount will be automatically rolled back.

II. implementation of account lock
The locking mechanism of the database is used here. the specific logic is not mentioned. the code is as follows:

Class AppLock {function _ construct ($ appid) {$ this-> m_appid = $ appid; // initialize data $ 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 ($ r Et 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 ;} // Obtain data again $ 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 ();} // whether the lock public $ m_bGot = false is obtained; public $ m_appid ;}

To prevent deadlocks, the lock retrieval logic adds the timeout time judgment. you can see the code as needed.

III. reconciliation logic
If you follow the above system design, you only need to check the successful traffic (bill_ret = 0) on both sides during reconciliation. if they are completely consistent, there should be no problem with the account, if they are inconsistent, check the problem.
Some colleagues told me that when I was working in the company, I used to take all the records in the flow table before any write operations, accumulate the amt value to see if the result is the same as the balance. If they are different, the problem may occur.
Select sum (amt) from tb_bill where appid = 1;
Therefore, this is why the amt field in the Streamline table is to distinguish between positive and negative.

The above is all the content in this article. I hope it will help you learn more. For more information, please follow the PHP Chinese network (www.php1.cn )!

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.