"""模块:支付 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')