Files
Django/gvsdsdk/payment/models.py

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')