195 lines
6.9 KiB
Python
195 lines
6.9 KiB
Python
"""模块:支付 SAAS 数据模型
|
|
SAAS 级支付配置与交易流水——不绑定具体业务。
|
|
子服务自行关联 payment_transaction_id 到自己的订单。"""
|
|
|
|
import uuid
|
|
import datetime
|
|
from django.db import models
|
|
from gvsdsdk.model_base import QModel
|
|
from gvsdsdk.models import UUIDField
|
|
|
|
|
|
class PaymentGatewayConfig(QModel):
|
|
"""支付网关配置表——每个租户每个渠道一条配置
|
|
|
|
存储加密后的支付密钥,子服务通过此表配置自己的支付能力。
|
|
SAAS 主服务器不关心具体业务,只提供通道。
|
|
"""
|
|
|
|
ConfigUUID = UUIDField(primary_key=True)
|
|
TenantUUID = UUIDField()
|
|
Platform = models.CharField(max_length=32)
|
|
AppID = models.CharField(max_length=64, default='')
|
|
MchID = models.CharField(max_length=64, default='')
|
|
APIKeyEncrypted = models.CharField(max_length=512, default='')
|
|
PrivateKeyEncrypted = models.TextField(default='')
|
|
PublicKeyEncrypted = models.TextField(default='')
|
|
CertPath = models.CharField(max_length=512, default='')
|
|
CertKeyPath = models.CharField(max_length=512, default='')
|
|
NotifyURL = models.CharField(max_length=512, default='')
|
|
ReturnURL = models.CharField(max_length=512, default='')
|
|
ConfigExtra = models.JSONField(default=dict)
|
|
ConfigStatus = models.SmallIntegerField(default=1)
|
|
ConfigDefault = models.BooleanField(default=False)
|
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
|
UpdateTime = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
db_table = 'PaymentGatewayConfig'
|
|
app_label = 'gvsdsdk'
|
|
verbose_name = '支付网关配置'
|
|
verbose_name_plural = '支付网关配置'
|
|
unique_together = [('TenantUUID', 'Platform')]
|
|
managed = False
|
|
indexes = [
|
|
models.Index(fields=['TenantUUID'], name='idx_paycfg_tenant'),
|
|
models.Index(fields=['Platform'], name='idx_paycfg_platform'),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f'{self.Platform}@{self.TenantUUID}'
|
|
|
|
@property
|
|
def IsActive(self):
|
|
return self.ConfigStatus == 1
|
|
|
|
def DecryptAPIKey(self):
|
|
if not self.APIKeyEncrypted:
|
|
return ''
|
|
return _decrypt_field(self.APIKeyEncrypted)
|
|
|
|
def DecryptPrivateKey(self):
|
|
if not self.PrivateKeyEncrypted:
|
|
return ''
|
|
return _decrypt_field(self.PrivateKeyEncrypted)
|
|
|
|
def DecryptPublicKey(self):
|
|
if not self.PublicKeyEncrypted:
|
|
return ''
|
|
return _decrypt_field(self.PublicKeyEncrypted)
|
|
|
|
@classmethod
|
|
def FromTenantAndPlatform(cls, tenant_uuid, platform):
|
|
try:
|
|
return cls.objects.get(TenantUUID=tenant_uuid, Platform=platform, ConfigStatus=1)
|
|
except cls.DoesNotExist:
|
|
return None
|
|
|
|
|
|
class PaymentTransaction(QModel):
|
|
"""支付交易流水表——SAAS 级统一交易记录
|
|
|
|
每笔支付/退款/转账操作都在此记录一条流水。
|
|
子服务通过此表的 TransactionUUID 关联自己的业务订单。
|
|
"""
|
|
|
|
TX_PAY = 'pay'
|
|
TX_REFUND = 'refund'
|
|
TX_TRANSFER = 'transfer'
|
|
|
|
TX_TYPE_CHOICES = [
|
|
(TX_PAY, '支付'),
|
|
(TX_REFUND, '退款'),
|
|
(TX_TRANSFER, '转账'),
|
|
]
|
|
|
|
TX_PENDING = 0
|
|
TX_SUCCESS = 1
|
|
TX_FAILED = 2
|
|
TX_CLOSED = 3
|
|
|
|
TX_STATUS_CHOICES = [
|
|
(TX_PENDING, '处理中'),
|
|
(TX_SUCCESS, '成功'),
|
|
(TX_FAILED, '失败'),
|
|
(TX_CLOSED, '已关闭'),
|
|
]
|
|
|
|
TransactionUUID = UUIDField(primary_key=True)
|
|
TenantUUID = UUIDField()
|
|
Platform = models.CharField(max_length=32)
|
|
TransactionType = models.CharField(max_length=16, choices=TX_TYPE_CHOICES)
|
|
OutTradeNO = models.CharField(max_length=64, unique=True)
|
|
OutRefundNO = models.CharField(max_length=64, default='')
|
|
ChannelTransactionID = models.CharField(max_length=128, default='')
|
|
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
|
|
Currency = models.CharField(max_length=10, default='CNY')
|
|
TransactionStatus = models.SmallIntegerField(
|
|
default=TX_PENDING, choices=TX_STATUS_CHOICES,
|
|
)
|
|
Subject = models.CharField(max_length=256, default='')
|
|
RawRequest = models.JSONField(default=dict)
|
|
RawResponse = models.JSONField(default=dict)
|
|
NotifyData = models.JSONField(default=dict)
|
|
PayTime = models.DateTimeField(null=True, blank=True)
|
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
|
UpdateTime = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
db_table = 'PaymentTransaction'
|
|
app_label = 'gvsdsdk'
|
|
verbose_name = '支付交易流水'
|
|
verbose_name_plural = '支付交易流水'
|
|
ordering = ['-CreateTime']
|
|
managed = False
|
|
indexes = [
|
|
models.Index(fields=['TenantUUID', 'CreateTime'], name='idx_paytx_tenant_time'),
|
|
models.Index(fields=['Platform', 'TransactionStatus'], name='idx_paytx_platform_status'),
|
|
models.Index(fields=['OutTradeNO'], name='idx_paytx_out_trade_no'),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.OutTradeNO
|
|
|
|
@property
|
|
def IsSuccess(self):
|
|
return self.TransactionStatus == self.TX_SUCCESS
|
|
|
|
def MarkSuccess(self, channel_transaction_id='', pay_time=None, notify_data=None):
|
|
self.TransactionStatus = self.TX_SUCCESS
|
|
self.ChannelTransactionID = channel_transaction_id
|
|
self.PayTime = pay_time or datetime.datetime.utcnow()
|
|
if notify_data:
|
|
self.NotifyData = notify_data
|
|
self.UpdateTime = datetime.datetime.utcnow()
|
|
self.save(update_fields=[
|
|
'TransactionStatus', 'ChannelTransactionID',
|
|
'PayTime', 'NotifyData', 'UpdateTime',
|
|
])
|
|
|
|
def MarkFailed(self, raw_response=None):
|
|
self.TransactionStatus = self.TX_FAILED
|
|
if raw_response:
|
|
self.RawResponse = raw_response
|
|
self.UpdateTime = datetime.datetime.utcnow()
|
|
self.save(update_fields=['TransactionStatus', 'RawResponse', 'UpdateTime'])
|
|
|
|
def MarkClosed(self):
|
|
self.TransactionStatus = self.TX_CLOSED
|
|
self.UpdateTime = datetime.datetime.utcnow()
|
|
self.save(update_fields=['TransactionStatus', 'UpdateTime'])
|
|
|
|
|
|
def _decrypt_field(encrypted_value):
|
|
import base64
|
|
from django.conf import settings
|
|
key = getattr(settings, 'PAYMENT_ENCRYPTION_KEY', 'SMANAGE-payment-default-key-32b')
|
|
try:
|
|
raw = base64.b64decode(encrypted_value)
|
|
return bytes(a ^ b for a, b in zip(raw, key.encode() * (len(raw) // len(key.encode()) + 1))).decode('utf-8')
|
|
except Exception:
|
|
return encrypted_value
|
|
|
|
|
|
def EncryptField(plain_value):
|
|
import base64
|
|
from django.conf import settings
|
|
if not plain_value:
|
|
return ''
|
|
key = getattr(settings, 'PAYMENT_ENCRYPTION_KEY', 'SMANAGE-payment-default-key-32b')
|
|
raw = bytes(a ^ b for a, b in zip(
|
|
plain_value.encode('utf-8'),
|
|
key.encode() * (len(plain_value.encode('utf-8')) // len(key.encode()) + 1),
|
|
))
|
|
return base64.b64encode(raw).decode('utf-8')
|