PHP支付系统设计与典型案例分享_php实例

由于公司业务需要,花两周时间实现了一个小型的支付系统,麻雀虽小五脏俱全,各种必须的模块如账户加锁,事务性保证,流水对帐等都是有完整实现的,整个开发过程中有很多经验积累,再加上在网上搜索了一下,大部分都是些研究性的论文,对实际使用价值不大,所以这次特意拿出来和大家分享一下。
这个系统可以用作小型支付系统,也可以用做第三方应用接入开放平台时的支付流水系统。
原来的需求比较负责,我简化一点说:

对每个应用,对外需要提供 获取余额,支付设备,充值 等接口
后台有程序,每月一号进行清算
账户可以被冻结
需要记录每一次操作的流水,每天的流水都要和发起方进行对账

针对上面的需求,我们设置如下数据库:

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

详细解释如下:
tb_status 应用的状态表。负责账户是否被冻结,账户的类型是什么(真实的需求是应用可能有两种账户,这里为简单所以没有列出)
appid 应用id
freeze 是否冻结
create_time 创建时间
change_time 最后一次修改时间
tb_account_earn 应用的账户余额表
appid 应用id
balance 余额(单位为分,不要用小数存储,因为小数本身不精确;另外php要在64位机下才能支持bigint)
create_time 创建时间
change_time 最后一次修改时间
seqid 操作序列号(防并发,每次update都会+1)
tb_assign 分配流水id的表,tb_bill的bill_id必须是有tb_assign分配的
id 自增id
create_time 创建时间
tb_bill 流水表。负责记录每一条操作流水,这里的bill_id不是主键,因为同一个bill_id可能会有支付和回滚两条流水
id 自增序列号
bill_id 流水号
amt 操作的金额(这个是要区别正负的,主要是为了select all的时候可以直接计算出某段时间的金额变化)
bill_info 操作的详细信息,比如3台webserver,2台db
bill_user 操作用户
bill_time 流水时间
bill_type 流水类型,区分是加钱还是减钱
bill_channel 流水来源,如充值,支付,回滚,结算还是其他
bill_ret 流水的返回码,包括未处理、成功、失败,这里的逻辑会在后面讲解
appid 应用id
old_balance 操作发生前的账户余额
price_info 记录操作发生时,记录被支付物品的单价
src_ip 客户端ip
tb_price 单价表,记录了机器的单价
name 机器唯一标识
price 价格
info 描述
tb_applock 锁定表,这是为了避免并发对某一个应用进行写操作设计的,具体的代码会在后面展示
appid 应用id
lock_mode 锁定状态。为0则为锁定,为1则为锁定
change_time 最后一次修改时间
OK,库表设计出来之后,我们就来看一下最典型的几个操作.

一. 支付操作
我这里只列出了我目前实现的方式,可能不是最好的,但应该是最经济又满足需求的。
先说调用方这里,逻辑如下:

然后对应的支付系统内部逻辑如下(只列出支付操作,回滚逻辑差不多,流水检查是要检查对应的支付流水是否存在):

常用的错误返回码可能如下就足够了:

$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('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()) <= 0)
{
$applock_data = array(
'appid'=>$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()) <= 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();
} 

//是否获取到了锁
public $m_bGot = false; 

public $m_appid;
}

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索php支付设计
php支付系统设计
excel vba典型实例368、indesign典型实例、典型铸件工艺设计实例、vba典型实例大全、物联网应用的典型实例,以便于您获取更多的相关知识。

时间: 2024-08-30 08:36:20

PHP支付系统设计与典型案例分享_php实例的相关文章

PHP支付系统设计与典型案例分享

由于公司业务需要,花两周时间实现了一个小型的支付系统,麻雀虽小五脏俱全,各种必须的模块如账户加锁,事务性保证,流水对帐等都是有完整实现的,整个开发过程中有很多经验积累,再加上在网上搜索了一下,大部分都是些研究性的论文,对实际使用价值不大,所以这次特意拿出来和大家分享一下. 这个系统可以用作小型支付系统,也可以用做第三方应用接入开放平台时的支付流水系统. 原来的需求比较负责,我简化一点说: 对每个应用,对外需要提供 获取余额,支付设备,充值 等接口 后台有程序,每月一号进行清算 账户可以被冻结 需

PHP+memcache实现消息队列案例分享_php实例

memche消息队列的原理就是在key上做文章,用以做一个连续的数字加上前缀记录序列化以后消息或者日志.然后通过定时程序将内容落地到文件或者数据库. php实现消息队列的用处比如在做发送邮件时发送大量邮件很费时间的问题,那么可以采取队列.方便实现队列的轻量级队列服务器是:starling支持memcache协议的轻量级持久化服务器https://github.com/starling/starlingBeanstalkd轻量.高效,支持持久化,每秒可处理3000左右的队列http://kr.gi

RDS PostgreSQL\HDB PG 毫秒级海量时空数据透视 典型案例分享

标签 PostgreSQL , GIS , 时空数据 , 数据透视 , bitmapAnd , bitmapOr , multi-index , 分区 , brin , geohash cluster 背景 随着移动终端的普及,现在有越来越多的业务数据会包含空间数据,例如手机用户的FEED信息.物联网.车联网.气象传感器的数据.动物的溯源数据,一系列跟踪数据. 这些数据具备这几个维度的属性: 1.空间 2.时间 3.业务属性,例如温度.湿度.消费额.油耗.等. 数据透视是企业BI.分析师.运营非

VMware云安全典型案例分享

VMware云安全典型案例分享 臧铁军  VMware云安全产品与家 •案例1:以合规性为基础的自劢化运维管理•案例2:新一代纵深防御体系架构保障网络安全•案例3:桌面云劣力终端安全和数据安全 VMware云安全典型案例分享

thinkphp整合微信支付代码分享_php实例

本文实例为大家分享了thinkphp整合微信支付代码,供大家参考,具体内容如下 下载:支付SDK   将微信支付SDK放在第三方类库Vendor下面,请切记把WxPay.Config.php里面的商户信息修改为您的公众号信息,以避免造成资金的流失. php端代码 public function pay(){ //商户基本信息,可以写死在WxPay.Config.php里面,其他详细参考WxPayConfig.php vendor('Pay.JSAPI'); $tools = new \JsApi

PostgreSQL\GPDB 多维数据透视典型案例分享

标签 PostgreSQL , 数据透视 , 实时 , 物化 , 预计算 , 多维分析 , 流计算 , 增量合并 , 调度 , HLL 背景 典型的电商类数据透视业务,透视的语料可能会包含一些用户的标签数据:例如包含品牌的ID,销售区域的ID,品牌对应用户的ID,以及若干用户标签字段,时间字段等. 标签可能会按不同的维度进行归类,例如tag1 性别,tag2 年龄段, tag3 兴趣爱好, .... 业务方较多的需求可能是对自有品牌的用户进行透视,统计不同的销售区域(渠道).时间段.标签维度下的

PHP开发微信支付的代码分享_php实例

微信支付,即便交了保证金,你还是处理测试阶段,不能正式发布.必须到你通过程序测试提交订单.发货通知等数据到微信的系统中,才能申请发布. 然后,因为在微信中是通过JS方式调用API,必须在微信后台设置支付授权目录,而且要到二级三级目录下去,这对于使用MVC框架来说,是个小问题. 使用MVC,在开发环境,url往往是native url格式,不能保证一定是带有路径/的形式 所以,比较可靠的方式,是创建实体的子文件夹,这样的化,在系统整体url方面,需要进行一些改造. 另外,最后一步,js调用时,对于

ThinkPHP中数据操作案例分析_php实例

本文实例分析了ThinkPHP中数据操作方法.分享给大家供大家参考.具体如下: 模板中使用函数,相信大部分的TPer都知道也用过吧. 案例: 获取用户名的方法 function get_username($uid){ $row = M('User')->getbyUid($uid); return $row['username']; } 在模板中通过uid显示用户名 <volist name="rows" id="row"> <tr>

PHP在线书签系统分享_php实例

本文为大家分享了PHP在线书签系统,感兴趣的小伙伴们可以参考一下 1.需求分析 首先,需要识别每个用户.应该有验证机制. 其次,需要保存单个用户的书签.用户应该能够添加和删除书签. 再次,需要根据对他们的了解,向用户建议他们可能感兴趣的站点. 2.解决方案2.1 系统流程图 2.2 PHPbookmark中的文件列表 3.实现数据库 create database bookmarks; use bookmarks; create table user ( username varchar(16)