>  기사  >  백엔드 개발  >  PHP 결제 시스템 설계 및 일반적인 사례 Sharing_php 예제

PHP 결제 시스템 설계 및 일반적인 사례 Sharing_php 예제

PHP中文网
PHP中文网원래의
2016-08-17 13:02:33860검색

회사의 비즈니스 요구로 인해 소액 결제 시스템을 구현하는 데 2주가 걸렸습니다. 비록 규모는 작지만 계정 잠금, 거래 보장, 화해 등 필요한 모든 모듈이 완벽하게 구현되어 있습니다. , 개발 전 과정에 걸쳐 많은 경험을 쌓아왔고, 인터넷으로 검색해본 결과 대부분이 연구 논문이어서 실용 가치가 거의 없어 이번에 특별히 공유합니다.
본 시스템은 소액결제 시스템으로 활용이 가능하며, 오픈플랫폼에 타사 애플리케이션이 연결되면 결제 플로우 시스템으로 활용될 수 있습니다.
원래 수요가 더 책임감이 있으므로 단순화하여 이렇게 말합니다.

각 애플리케이션에 대해 외부 요구는 잔고 확보, 장비 지불, 재충전 제공해야 합니다. 인터페이스를 기다립니다
백그라운드에 프로그램이 있으며 매월 1일 청산이 진행됩니다
계정이 동결될 수 있습니다
각 작업의 흐름을 기록해야 합니다. , 일일 흐름은 개시자와 조정되어야 합니다

위 요구 사항에 따라 다음 데이터베이스를 설정합니다.

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
동결 여부
create_time 생성 시간
change_time 마지막 수정 시간
tb_account_earn 애플리케이션 계정 잔액 테이블
appid 애플리케이션 ID
잔액 잔액(단위는 센트입니다. 소수점 자체가 정확하지 않으므로 소수점 저장을 사용하지 마세요. 또한 PHP는 bigint를 지원할 수 있는 64비트 시스템에 있어야 합니다.)
create_time 생성 시간
change_time 마지막 수정 시간
seqid 작업 시퀀스 번호(동시 방지, 모든 업데이트는 +1이 됩니다)
tb_sign 테이블은 Serial id를 할당하기 위한 테이블로, tb_bill의 bill_id는 tb_sign
id auto-increment id
create_time 생성시간
tb_bill 플로우 테이블로 할당해야 한다. 각 작업 흐름을 기록하는 책임은 동일한 bill_id에 결제 및 롤백이라는 두 가지 흐름이 있을 수 있기 때문입니다.
bill_id 일련 번호
금액 연산 (이것은 양수와 음수의 구별이 필요하며 주로 모두 선택 시 일정 기간 동안의 양의 변화를 직접 계산하기 위함입니다)
bill_info 웹서버 3개, db 2개 등 연산의 상세 정보
bill_user 운영 사용자
bill_time 실행 시간
bill_type 돈을 추가하거나 차감할지 구분하는 실행 유형
bill_channel 충전, 결제, 롤백, 정산 등의 유수 소스
bill_ret 미처리 및 성공을 포함한 유수의 반환 코드입니다. 실패했습니다. 여기서의 논리는 나중에 설명됩니다
appid 애플리케이션 ID
old_balance 작업 발생 전 계정 잔액
price_info의 단가를 기록합니다. 작업 발생 시 지급되는 항목
src_ip 클라이언트 IP
tb_price 단가 테이블, 기계 단가 기록
이름 기계 고유 식별자
가격 가격
정보 설명
tb_applock 잠금 애플리케이션에 대한 동시 쓰기 작업을 방지하도록 설계된 테이블입니다. 특히 이 코드는 나중에
appid 애플리케이션 ID
lock_mode 잠금 상태를 표시합니다. 0이면 잠김, 1이면 잠김
change_time 마지막 수정 시간
자, 라이브러리 테이블이 디자인되면 가장 일반적인 동작을 살펴보겠습니다.

1. 결제 운영
현재 구현하고 있는 방식만 나열합니다. 최고는 아닐 수 있지만 가장 경제적이며 요구 사항을 충족해야 합니다.
먼저 발신자에 대한 이야기를 하자면

PHP 결제 시스템 설계 및 일반적인 사례 Sharing_php 예제

그러면 해당 결제 시스템의 내부 로직은 다음과 같습니다(결제 작업만 나열되어 있으며, 롤백 로직은 유사하며 흐름 확인은 해당 결제 흐름이 있는지 확인하는 것입니다):

PHP 결제 시스템 설계 및 일반적인 사례 Sharing_php 예제

일반적으로 사용되는 오류 반환 코드는 다음과 같습니다.


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

결제 작업을 수행할 때 0보다 큰 오류는 논리 오류로 간주되며 호출자는 거래를 기록할 필요가 없습니다. 계정이 변경되지 않았기 때문입니다.

0 미만의 오류는 내부 시스템 오류입니다. 데이터 변경 여부를 알 수 없으므로 호출자와 결제 시스템 모두 흐름을 기록해야 합니다.
반환이 0이면 성공을 의미하며 양쪽 모두 흐름을 기록해야 합니다.
결제 시스템에서는 거래를 먼저 작성한 다음 계정을 업데이트하는 이유가 있습니다. 쉽게 말하면 거래 손실을 최대한 방지하기 위함입니다.
마지막으로 정리하자면 먼저 돈을 차감한 뒤 배송하고 문제가 있으면 롤백하는 방식도 있는데, 먼저 원천징수한 뒤 배송하고 문제가 없으면 취소하는 방식도 있다. 결제에 문제가 있을 경우 결제 취소를 위해 결제 확인이 호출되며, 보류 후 장기간 확인이 없을 경우 해당 금액이 자동으로 취소됩니다.

2. 계정 잠금 구현 여기서는 데이터베이스의 잠금 메커니즘이 사용됩니다. 코드는 다음과 같습니다. >

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; 
}

교착상태를 방지하기 위해 잠금 획득 로직에 타임아웃 판정을 추가했습니다.

조정 로직
위 시스템에 따라 설계되었다면 계좌 조정 시 양측의 성공적인 거래(예: bill_ret=0)만 비교하면 됩니다. 완전히 일치하면 문제가 없습니다. 계정과 일치하지 않는 경우 문제를 확인했습니다.
계정의 정확성을 보장하는 것과 관련하여 동료는 제가 회사에서 일할 때 쓰기 작업 전에 거래 테이블의 모든 거래 기록이 가져오고 amt의 가치가 있다고 말했습니다. 결과가 잔액과 동일한지 확인합니다. 동일하지 않다면 뭔가 잘못된 것입니다.
appid=1;
따라서 내 흐름 테이블에서 amt 필드가 양수와 음수를 구별해야 하는 이유는 tb_bill에서 sum(amt)를 선택하는 것입니다.

이상은 이 글의 전체 내용입니다. 모든 분들의 학습에 도움이 되길 바랍니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 주목해주세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.