Maison  >  Article  >  développement back-end  >  Conception du système de paiement PHP et cas typiques (recommandé)

Conception du système de paiement PHP et cas typiques (recommandé)

墨辰丷
墨辰丷original
2018-06-01 15:56:021979parcourir

Cet article présente principalement en détail la conception et les cas typiques du système de paiement PHP. Il peut être utilisé comme petit système de paiement ou comme système de flux de paiement lorsque des applications tierces sont connectées à la plateforme ouverte. valeur. Les amis intéressés peuvent se référer à

En raison des besoins commerciaux de l'entreprise, il a fallu deux semaines pour mettre en œuvre un petit système de paiement. Bien qu'il soit petit, il dispose de tous les modules nécessaires, tels que le verrouillage de compte et la garantie transactionnelle. , le rapprochement de l'eau courante, etc. sont entièrement mis en œuvre. Il y a beaucoup d'expérience accumulée dans l'ensemble du processus de développement. De plus, après une recherche sur Internet, la plupart d'entre eux sont des documents de recherche et ont peu de valeur pratique, donc cette fois je. Je l'ai spécialement sorti pour le partager avec vous.
Ce système peut être utilisé comme petit système de paiement ou comme système de flux de paiement lorsque des applications tierces sont connectées à la plateforme ouverte.
La demande initiale est plus responsable, je vais la simplifier un peu :

Pour chaque application, des besoins externes pour subvenir obtenir le solde, payer le matériel, recharger Attendez l'interface
Il y a un programme en arrière-plan, et la liquidation sera effectuée le premier de chaque mois
Les comptes peuvent être gelés
Il est nécessaire d'enregistrer le flux de chaque opération , et le flux quotidien doit être rapproché avec l'initiateur

En réponse aux exigences ci-dessus, nous avons mis en place la base de données suivante :

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());

L'explication détaillée est la suivante :
tb_status Table d'état de l'application. Responsable de savoir si le compte est gelé et de quel type de compte il s'agit (la véritable exigence est que l'application puisse avoir deux comptes, elle n'est donc pas répertoriée ici par souci de simplicité)
identifiant de l'application appid
geler s'il faut geler
create_time heure de création
change_time heure de la dernière modification
tb_account_earn table de solde du compte d'application
appid application id
balance balance (l'unité est en cent, n'utilisez pas de stockage décimal, car la décimale elle-même n'est pas exacte ; de plus, PHP doit être sur une machine 64 bits peut prendre en charge bigint)
heure de création create_time
heure de dernière modification change_time
numéro de séquence d'opération seqid (anti-concurrence, chaque mise à jour sera +1)
Table tb_assign pour attribuer l'identifiant de série, bill_id de tb_bill Doit être attribué par tb_assign
ID d'incrémentation automatique
temps de création create_time
table de flux tb_bill. Responsable de l'enregistrement de chaque flux d'opération. Le bill_id ici n'est pas la clé primaire, car le même bill_id peut avoir deux flux, paiement et restauration
numéro de série à incrémentation automatique de l'identifiant
numéro de série bill_id
montant de. opération (ceci Il faut faire la distinction entre positif et négatif, principalement pour calculer directement la variation du montant pendant une certaine période de temps lors de la sélection de tout)
bill_info informations détaillées sur l'opération, telles que 3 serveurs web, 2 db
bill_user utilisateur opérationnel
bill_time L'heure de la transaction
bill_type Le type de transaction, distinguant s'il faut ajouter de l'argent ou soustraire de l'argent
bill_channel La source de la transaction, telle que la recharge, le paiement, l'annulation, le règlement ou autres
bill_ret Le code retour de la transaction, y compris non traité et réussi, a échoué, la logique ici sera expliquée plus tard
identifiant de l'application appid
old_balance solde du compte avant l'opération
price_info enregistre le prix unitaire de l'article être payé lorsque l'opération se produit
src_ip client ip
tb_price table de prix unitaire, enregistre le prix unitaire de la machine
nom identifiant unique de la machine
prix prix
info description
tb_applock lock table , qui est conçu pour éviter les opérations d'écriture simultanées sur une application, en particulier le code affichera l'
appid application id
lock_mode lock status plus tard. S'il vaut 0, il est verrouillé, s'il vaut 1, il est verrouillé
change_time Heure de la dernière modification
OK, une fois la table de la bibliothèque conçue, jetons un œil aux opérations les plus typiques.

1. Opération de paiement
Je n'énumère que la façon dont je la mets en œuvre actuellement. Ce n'est peut-être pas la meilleure, mais elle devrait être la plus économique et répondre aux besoins.
Parlons d'abord de l'appelant. La logique est la suivante :

Ensuite, la logique interne correspondante du système de paiement est la suivante (seules les opérations de paiement sont répertoriées, la logique de restauration est similaire et la vérification du flux est Pour vérifier si le flux de paiement correspondant existe) :

Les codes retour d'erreur couramment utilisés peuvent être les suivants :

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

对于大于0的错误都算是逻辑错误,执行支付操作,调用方是不用记录流水的。因为账户并没有发生任何改变。
对于小于0的错误是系统内部错误,因为不知道是否发生了数据更改,所以调用方和支付系统都要记录流水。
对于等于0的返回,代表成功,两边也肯定要记录流水。
而在支付系统内部,之所以采用先写入流水,再进行账户更新的方式也是有原因的,简单来说就是尽量避免丢失流水。
最后总结一下,这种先扣钱,再发货,出问题再回滚的方式是一种模式;还有一种是先预扣,后发货,没有出问题则调用支付确认来扣款,出了问题就调用支付回滚来取消,如果预扣之后很长时间不做任何确认,那么金额会自动回滚。

二. 账户锁定的实现
这里利用了数据库的加锁机制,具体逻辑就不说了,代码如下:

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 <= 0) 
{ 
app_error_log("applock alloc fail,affected_rows:$ret"); 
return false; 
} 
$this->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 <= 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(&#39;appid&#39;, $appid); 
  
$query = $db->get(TB_APPLOCK); 
  
if ($query === false) 
{ 
app_error_log("AppLock get fail.appid:$appid"); 
return false; 
} 
  
if (count($query->result_array()) <= 0) 
{ 
$applock_data = array( 
&#39;appid&#39;=>$appid, 
&#39;lock_mode&#39;=>APPLOCK_MODE_FREE, 
); 
$db->set(&#39;change_time&#39;,&#39;NOW()&#39;,false); 
$ret = $db->insert(TB_APPLOCK, $applock_data); 
if ($ret === false) 
{ 
app_error_log("applock insert fail:$appid"); 
return false; 
} 
  
//重新获取数据 
$db->where(&#39;appid&#39;, $appid); 
$query = $db->get(TB_APPLOCK); 
  
if ($query === false) 
{ 
app_error_log("AppLock get fail.appid:$appid"); 
return false; 
} 
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(&#39;appid&#39;,$appid); 
$db->where(&#39;lock_mode&#39;,$old_lock_mode); 
  
$db->set(&#39;lock_mode&#39;,$new_lock_mode); 
$db->set(&#39;change_time&#39;,&#39;NOW()&#39;,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; 
}

为了防止死锁的问题,获取锁的逻辑中加入了超时时间的判断,大家看代码应该就能看懂

三. 对帐逻辑
如果按照上面的系统来设计,那么对帐的时候,只要对一下两边成功(即bill_ret=0)的流水即可,如果完全一致那么账户应该是没有问题的,如果不一致,那就要去查问题了。
关于保证账户正确性这里,也有同事跟我说,之前在公司做的时候,是采取只要有任何写操作之前,都先取一下流水表中所有的流水记录,将amt的值累加起来,看得到的结果是否和余额相同。如果不相同应该就是出问题了。
select sum(amt) from tb_bill where appid=1;
所以这也是为什么我在流水表中,amt字段是要区分正负的原因。

总结:以上就是本篇文的全部内容,希望能对大家的学习有所帮助。

相关推荐:

php实现获取当前月与上个月月初及月末时间戳的方法

php+ajax实现全选删除的方法

php+ajax无刷新上传图片的实现方法

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn