0, the words written in the front
Payment systems are an old topic, and I believe that every company develops a different payment system because the business is not the same.
Here, I don’t want to talk about a large and comprehensive payment system, nor do I have the ability to do so personally.
In my opinion, a payment system should provide payment channel management, payment gateway, basic payment/refund/transfer capability, payment record/detail, and related monitoring and maintenance system.
As for the so-called account clearing, reconciliation function, account system, risk control system, cash flow management, should be incorporated into the “financial system”, probably is the big guys are talking about the broad “payment system”!
Today, I will only talk about “payment systems” in the narrow sense.
Currently, the payment process consists of three major parts: initiating payment, initiating refund and receiving callback.
Switching from synchronous to asynchronous programming for throughput concerns will, unsurprisingly, take advantage of Java8’s ExecutorService and CompletableFuture.
In addition, the company’s other off-the-shelf products were used: RabbitMQ, Redis, MongoDB.
My intention is to design the payment system so that it is business-related and can be integrated into the company’s public platform system.
Read on to find out how.
1. Initiate payment
This section describes how the client and server work together to complete a payment request. The server must be aware that it is the client that ultimately initiates the payment and that the server provides the necessary configuration information.
The architecture diagram for initiating payment is as follows:
Following the marked Sequence number, you can trace how a payment request is initiated (without needing to know the Sequence Diagram). The flow is described as follows:
- Submit a Pay task. When the client needs to initiate payment, it starts by adding a new payment task to the payment task queue, and this process is realized asynchronously. Firstly, according to the parameters submitted by the client, a new payment task is constructed.
- Offer a task, which starts an asynchronous task that adds a new payment task to MQ and waits to be consumed;
- Pay task description: once the asynchronous task is successfully created, the payment task information constructed in the first step will be directly returned to the client.
- Poll a task, at the same time, consumers of the payment task Poll down to perform the new payment task;
- Send a Pay Request. This step depends on the actual situation. Not all payment requests go through third-party payment platforms, such as Alipay; For wechat, it is necessary to apply for a prepay_id based on payment parameters, and then initiate payment through the client.
- Response, nothing to say, third-party channels return the necessary parameters for payment;
- Cache result, at this point, a payment task can be regarded as completed, the execution result of the task (whether successful or not) can be cached in Redis, waiting for the return visit of the client;
- Query result: after the client submits the payment task, it initiates a result Query request after a certain interval (2~3s is recommended);
- Query, directly into Redis to find the result;
- Synchronize, an asynchronous operation that synchronizes the result of a payment task to MongoDB and deletes the result of the task cached in Redis. Persisting to MongoDB mainly provides landing data sources for subsequent fault tolerance, retry, data analysis, etc.
- Return, Redis returns to the application server.
- Return payment, and the application server returns the final payment object to the client.
Let’s dig a little deeper and look at three Class diagrams:
① Let’s talk about PayTask first. Both PayTask and Payment are Document objects in MongoDB, but during the execution of the task, PayTask is cached by Redis, which is convenient for the client to initiate Query at any time. After the task is successfully executed, Payment objects will be generated. Eventually both PayTask and Payment are persisted to MongoDB. In PayService, there are some basic operations for payment tasks, including task submission, cancellation, retry, build, and so on.
② Talk about the runner. This part is related to RabbitMQ, once a payment task has been created, it is placed in the task execution queue for the consumer to take out and execute. In TaskRunner, there are two basic interface methods: Run (Task) and retry(task), respectively. AbstractPayTaskRunner (AbstractPayTaskRunner) AbstractPayTaskRunner (AbstractPayTaskRunner) AbstractPayTaskRunner (AbstractPayTaskRunner) With regard to the Retry mechanism, the user can set whether to Retry or not, and once taskInfo. needRetry=true is set, the Retry mechanism is enabled. You can also set the number of retries (taskInfo.retryTimes), which is three by default, with intervals of 1s, 2s, and 3s, in an arithmetic sequence of tolerance 1. Of course, the user will not be allowed to retry indefinitely, the system has a built-in maximum number of retries, the maximum number of retries is 5.
Why five?
You feel, 1s, 2s, 3s, 4s, 5s, the whole request chain is stretched to 15s, this is a disaster for the client!!
③ Let’s talk about the payment channel. This part of the design is closely connected with specific payment channels, including payment parameter configuration, payment parameter processing, signature/check, etc.
④ Finally explain the payment parameters (PayParams).
Most of them are understandable, but let me explain a few key properties:
1) appId, which is set up to distinguish between different products. In reality, it is very likely that a product will apply for the corresponding payment channel, then create an application in the payment platform, set the corresponding payment parameters, and the system will assign an appId, by which each payment parameter can be directly located. If you want to be more sophisticated, you can distinguish between test and formal environments.
2) Amount, which represents the payment amount, but the unit of the payment system is uniformly set in RMB [fen];
Metadata, in theory, has no limit, if any, on the length of the field — 5000 characters. There’s a lot of room for imagination in this field: for filling in a wealth of transaction-related information and for in-depth business analysis in growing smart systems products. It includes multidimensional analysis of transaction behavior, crowd analysis, product transformation path, personalized recommendation, intelligent subsidy, targeted push, etc. It’s up to the product manager to play;
5) Credential, which is very, very important because it loads the credentials that the client ultimately initiates the Payment request and is returned to the client as part of the Payment object;
Document field design of MongoDB
Explain why MongoDB is used:
Personally, if this universal service is to be promoted well (even open source), relational database such as MySQL is the best choice, because a complete and practical system is inevitably inseparable from database. If some non-traditional things are used, it will inevitably increase the docking cost of some people. Some people just don’t fit in with the team’s technology stack and just drop it.
Why am I still using MongoDB?
① There is such a thing in the team’s technology stack that it is unnecessary to use it;
② the popularity of MongoDB is really not too high, there is no need to point NoSQL things, feel that they were OUT in a minute;
(3) The data structure to be stored needs to support the feature of dynamic expansion. I appreciate the flexibility of MongoDB. The data structure to be stored is as follows:
Document_name = “Payment”
{
"payId": "pay_Oyvrf9vP880STm1e9G5CSCm1"."method": "yoogurt.taxi.pay"."version": "v1.0"."timestamp": 1473044885,
"created": 1473042835,
"paid": false."appId": "app_KiPGa98abDmLe9ev"."channel": "wx"."orderNo": "20161899798416"."clientIp": "192.168.18.189"."amount": 10000,
"subject": "User recharge Order (¥100.0)"."body": "User recharge Order (¥100.0)"."paidTime": null,
"transactionNo": ""."metadata": {
"user_id": "170204469176"."phone_number": "13811234567"
},
"credential": {
"appId": "wx4932b5159d18311e"."partnerId": "1269774001"."prepayId": "wx201609051033574da13955420883291539"."nonceStr": "1e99d8ffdde926ed9cbdf4d2e614abad"."timeStamp": "1473042837"."packageValue": "Sign=WXPay"."sign": "1CECCE6B13C956DEBA88800B3DEC4DBE"
},
"extra": {},
"statusCode": ""."message": ""."description": ""
}
Copy the code
Metadata, credential, extra, and other fields do not have a particular fixed specification. MySQL requires redundant fields, or for each channel to separate tables, which is annoying to think about!
MySQL
Because this payment system is designed to support multiple applications and channels, MySQL is used here to store some application configurations. SQL > select * from db; SQL > select * from DB;
① Pay_channel: payment channel available for access
② APP_settings: payment application information
③ APP_channel: payment channel that the application has access to
④ Alipay_settings: Alipay parameter Settings
⑤ WX_Settings: Wechat app payment parameter Settings
If you want to increase the payment channels, you only need to add a corresponding payment parameter setting table.
2. Initiate a refund
Not surprisingly, customers can initiate refund for each order on the platform, and they can also refund in batches, that is, for the same order, they can initiate refund application for multiple times, as long as the total amount of refund does not exceed the total amount of actual payment. The architecture diagram is as follows:
There are many similarities with the process of initiating a payment request, and without explaining them all, there are two key points:
- When the client initiates a refund request, it needs to carry the payId, which is the ID of the payment object. This means that the caller of the payment system needs to maintain the correspondence between payId and orderNo and must obtain the correct payId before the client initiates the refund request.
- Following the previous step, this leads to steps 5 and 6 in the figure, which query the previous payment object from MongoDB. Third party channels usually require that a refund number be specified at the time of refund. Since one order can be refunded multiple times, it is not recommended to use the order number as the refund number. The refund number here is generated and maintained by the payment system.
The execution process of this part is similar to that before. The client initiates a Refund request, forms a RefundTask, and puts it into the task queue. Consumers take out and execute their business logic.
MongoDB
document_name = “Refund”
{
"payId": "pay_Oyvrf9vP880STm1e9G5CSCm1"."method": "yoogurt.taxi.pay"."version": "v1.0"."timestamp": 1473044885,
"created": 1473042835,
"refundId": "refund_kmw1vrf9wSrP1e9Gkp05CSCm1"."appId": "app_KiPGa98abDmLe9ev"."orderNo": "20161899798416"."clientIp": "192.168.18.189"."amount": 10000,
"succeedTime": 1473150835,
"transactionNo": "6405996874204000684260056054"."refundStatus": "success"."message": ""."metadata": {
"user_id": "170204469176"."phone_number": "13811234567"
},
"description": ""
}
Copy the code
3. Receive the callback
This part of the functionality is designed to be event-driven, so WebHooks are in charge.
Because the callbacks vary from channel to channel, this part of the design is segmented by payment channel.
The architecture diagram is as follows:
After the user completes the payment, the third-party payment channel will asynchronously notify the merchant of the successful payment through the callback address specified when initiating the payment.
The execution process of this part is similar to that before. The callback parameters are parsed in the respective PayChannel to form a callback Event, which is persisted in MongoDB, and then generated into a callback task (EventTask), which is put into the task queue, and consumers take out and execute their respective business logic. The consumer here is the upstream business service system.
MongoDB
Document_name = “Event”
{
"eventId": "evt_la06CoQAiPojSgJKe5gt3nwq"."created": 1427555016,
"eventType": "pay.succeeded"."data": {
"payId": "pay_Oyvrf9vP880STm1e9G5CSCm1"."method": "yoogurt.taxi.pay"."version": "v1.0"."timestamp": 1473044885,
"created": 1473042835,
"paid": false."appId": "app_KiPGa98abDmLe9ev"."channel": "wx"."orderNo": "20161899798416"."clientIp": "192.168.18.189"."amount": 10000,
"subject": "User recharge Order (¥100.0)"."body": "User recharge Order (¥100.0)"."paidTime": null,
"transactionNo": ""."statusCode": ""."message": ""."metadata": {
"user_id": "170204469176"."phone_number": "13811234567"
},
"credential": {
"appId": "wx4932b5159d18311e"."partnerId": "1269774001"."prepayId": "wx201609051033574da13955420883291539"."nonceStr": "1e99d8ffdde926ed9cbdf4d2e614abad"."timeStamp": "1473042837"."packageValue": "Sign=WXPay"."sign": "1CECCE6B13C956DEBA88800B3DEC4DBE"
},
"extra": {},"description": ""
},
"retryTimes": 0}Copy the code
In particular, the data field:
If it is a successful Payment event, the corresponding Payment object is returned.
If it is the time when the Refund is successful, the Refund object is returned.
conclusion
Some readers may feel that this is not a payment system, but only a third-party payment channel, barely a payment channel gateway bar!
If you feel that way, I agree with you.
I think this article is more grounded, there is not too much theory, see more is the realization of the level of the content, the poor paste code!
Frankly speaking, third-party payment channels have been connected for many times, but they have not been systematically designed and summarized as now.
I have used ping++ for several times. In the field of enterprise-level converged payment, ping++ is the industry leader. Therefore, some of my data structure design is somewhat similar to it, and ping++ will also be the object I imitate and compare.
This is also the first step to realize my payment system, and I will continue to enrich and improve my own payment system in the future.
Hope to help you!
THANKS!
Daily dry goods sharing, transmission of valuable information in the Internet world, wechat official account: Jishuhui_2015