Kaynağa Gözat

苹果内购, 循环扣款

linhaohong 9 ay önce
ebeveyn
işleme
dbddf94c88

+ 44 - 1
Controller/CloudStorage.py

@@ -48,6 +48,7 @@ from Service.PayService import PaymentService
 from Service.VodHlsService import SplitVodHlsObject
 from Object.ApplePayObject import ApplePayObject
 from Object.UnionPayObject import UnionPayObject
+from Object.AppleInAppPurchaseSubscriptionObject import InAppPurchase
 
 ssl._create_default_https_context = ssl._create_unverified_context
 LOGGER = logging.getLogger('info')
@@ -1695,7 +1696,49 @@ class CloudStorageView(View):
         elif pay_type == 5:
             # 内购订阅型订单 之前订阅过任意套餐返回不可再次订阅
             if store_qs[0]['cycle_config_id']:
-                return response.json(173)
+                # 检查Paypal订阅
+                check_subscribe_Paypal = Paypal.checkSubscriptions(user_id, uid, rank, app_type)
+                # 查设备是否有绑定套餐
+                device_apple_package_qs = DeviceApplePackage.objects.filter(userID=user_id, uid=uid).values(
+                    "package_id__product_id", "package_id__subscription_group_id")
+                if device_apple_package_qs.exists():
+                    product_id = device_apple_package_qs[0]["package_id__product_id"]
+                    subscription_group_id = device_apple_package_qs[0]["package_id__subscription_group_id"]
+
+                    # 检查内购订阅
+                    bundle_id = "com.ansjer.zccloud"
+                    if app_type == 2:
+                        bundle_id = "com.cloudlife.commissionf"
+                    in_app_subscription = InAppPurchase(bundle_id=bundle_id)
+                    check_subscribe_InApp = in_app_subscription.check_subscriptions(uid, subscription_group_id)
+
+                    if not check_subscribe_Paypal or check_subscribe_InApp:
+                        logger.info(
+                            f'设备订阅过套餐Paypal:{not check_subscribe_Paypal}, AppleInApp:{check_subscribe_InApp}')
+                        return response.json(10050)
+                else:
+                    # 查询所有符合条件的订阅套餐的 id 列表
+                    package_list = (InAppPurchasePackage.objects.filter(is_ai=is_ai, package_type=1)
+                                    .values_list("id", flat=True))
+                    # 查询用户和设备已绑定的套餐 id 列表
+                    bound_packages = DeviceApplePackage.objects.filter(userID=user_id).values_list("package_id",
+                                                                                                   flat=True)
+                    # 找到未绑定的套餐 id
+                    unbound_packages = set(package_list) - set(bound_packages)
+                    if not unbound_packages:
+                        return response.json(10050)
+                    # 选择一个未绑定的套餐 id
+                    package_id = unbound_packages.pop()
+                    # 绑定一个套餐
+                    DeviceApplePackage.objects.create(
+                        userID=user_id,
+                        uid=uid,
+                        package_id_id=package_id,
+                        created_time=int(time.time()),
+                        update_time=int(time.time())
+                    )
+                    product_id = InAppPurchasePackage.objects.filter(id=package_id).values("product_id")[0][
+                        "product_id"]
             else:
                 product_id = InAppPurchasePackage.objects.filter(rank_id=rank).values("product_id")[0]["product_id"]
 

+ 468 - 81
Controller/InAppPurchaseController.py

@@ -6,24 +6,27 @@ import time
 import json
 
 import requests
+from appstoreserverlibrary.models.Environment import Environment
 from appstoreserverlibrary.api_client import AppStoreServerAPIClient, GetTransactionHistoryVersion
+from appstoreserverlibrary.models.AccountTenure import AccountTenure
+from appstoreserverlibrary.models.ConsumptionRequest import ConsumptionRequest
+from appstoreserverlibrary.models.ConsumptionStatus import ConsumptionStatus
+from appstoreserverlibrary.models.DeliveryStatus import DeliveryStatus
 from appstoreserverlibrary.models.Environment import Environment
+from appstoreserverlibrary.models.LifetimeDollarsPurchased import LifetimeDollarsPurchased
+from appstoreserverlibrary.models.LifetimeDollarsRefunded import LifetimeDollarsRefunded
+from appstoreserverlibrary.models.Platform import Platform
+from appstoreserverlibrary.models.PlayTime import PlayTime
+from appstoreserverlibrary.models.RefundPreference import RefundPreference
+from appstoreserverlibrary.models.UserStatus import UserStatus
 from appstoreserverlibrary.receipt_utility import ReceiptUtility
-from appstoreserverlibrary.models.HistoryResponse import HistoryResponse
-from appstoreserverlibrary.models.TransactionHistoryRequest import TransactionHistoryRequest, ProductType, Order
-from appstoreserverlibrary.signed_data_verifier import SignedDataVerifier
-
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.serialization import load_pem_private_key
-
-from django.db.models import Q
 from django.views import View
 from django.http import HttpResponse
 
 from Ansjer.config import LOGGER, CONFIG_INFO, CONFIG_TEST, PAY_TYPE_IN_APP_PURCHASE, BASE_DIR, CONFIG_US
 from Controller.CheckUserData import DataValid
 from Model.models import Order_Model, Store_Meal, Device_Info, UID_Bucket, Unused_Uid_Meal, AiService, Device_User, \
-    SysMsgModel, InAppPurchasePackage
+    SysMsgModel, DeviceApplePackage, InAppPurchasePackage, InAppRefund
 from Object.AWS.S3Email import S3Email
 from Object.AliSmsObject import AliSmsObject
 from Object.AppleInAppPurchaseSubscriptionObject import InAppPurchase
@@ -31,6 +34,7 @@ from Object.RedisObject import RedisObject
 from Service.CommonService import CommonService
 
 ENV = Environment.SANDBOX if CONFIG_INFO == CONFIG_TEST else Environment.PRODUCTION
+logger = logging.getLogger('apple_pay')
 
 
 class InAppPurchaseView(View):
@@ -45,10 +49,14 @@ class InAppPurchaseView(View):
         return self.validation(request.POST, request, operation)
 
     def validation(self, request_dict, request, operation):
-        if operation == 'AppStoreServerNotifications':  # App Store服务器通知
-            return self.app_store_server_notifications(request)
-        elif operation == 'VseesNotifications':
+        if operation == 'AppStoreServerNotifications':  # App Store服务器通知(用于转发通知)
+            return self.app_store_server_notifications(request, request_dict)
+        elif operation == 'AppStoreServerNotificationsVsees':  # App Store服务器通知(用于转发通知)
+            return self.app_store_server_notifications_vsees(request, request_dict)
+        elif operation == 'vseesNotifications':
             return self.vsees_notifications(request)
+        elif operation == 'serverNotifications':  # App Store服务器通知
+            return self.server_notifications(request)
         token_code, user_id, response = CommonService.verify_token_get_user_id(request_dict, request)
         if token_code != 0:
             return response.json(token_code)
@@ -66,24 +74,30 @@ class InAppPurchaseView(View):
         @return: response
         """
         receipt = request_dict.get('receipt', None)
+        transaction_identifier = request_dict.get('transactionIdentifier', "")
+        original_transaction_identifier = request_dict.get('originalTransactionIdentifier', "")
         order_id = request_dict.get('orderID', None)
         uid = request_dict.get('uid', None)
         lang = request_dict.get('lang', 'en')
         channel = request_dict.get('channel', None)
         app_type = request_dict.get('appType', 1)
-        logger = logging.getLogger('apple_pay')
-        logger.info(f"receipt: {receipt}, 订单orderId: {order_id}, uid: {uid}")
 
-        if not all([receipt, uid, channel, order_id]):
+        logger.info(
+            f"苹果内购认证交易订单orderID:{order_id},"
+            f"transaction_id: {transaction_identifier},"
+            f"original_transaction_id: {original_transaction_identifier},"
+            f"receipt: {receipt}")
+
+        if not all([uid, channel, order_id]):
             return response.json(444)
 
         # redis加锁,防止订单重复
         redis_obj = RedisObject()
         redis_key = order_id + 'in_app_purchase'
         is_lock = redis_obj.CONN.setnx(redis_key, 1)
-        redis_obj.CONN.expire(redis_key, 60)
         if not is_lock:
             return response.json(5)
+        redis_obj.CONN.expire(redis_key, 60)
 
         try:
             # 检查商品id是否正确
@@ -94,6 +108,7 @@ class InAppPurchaseView(View):
                 bundle_id = "com.ansjer.zccloud"
             else:
                 return response.json(444, "app_type不存在")
+
             # 实例化订阅类
             in_app_purchase = InAppPurchase(bundle_id=bundle_id)
 
@@ -105,11 +120,14 @@ class InAppPurchaseView(View):
             signed_data_verifier = in_app_purchase.verifier
 
             # 解析收据(循环扣款时不需要这一步, 直接获取transaction_id)
-            transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt)
-            if transaction_id is None:
+            transaction_id = transaction_identifier
+            if transaction_identifier == "":
+                transaction_id = receipt_util.extract_transaction_id_from_app_receipt(receipt)
+            if not transaction_id:
+                logger.info(f"苹果内购认证交易订单orderID:{order_id}, 没有transaction_id")
                 pay_result_url = CommonService.get_payment_status_url(lang, 'fail')
                 return response.json(0, {'url': pay_result_url})
-            logger.info(f"订单orderId:{order_id}, transaction_id:{transaction_id}")
+            logger.info(f"苹果内购认证交易订单orderID:{order_id}, transaction_id:{transaction_id}")
 
             # 查询交易信息
             transaction_info = client.get_transaction_info(transaction_id)
@@ -120,11 +138,13 @@ class InAppPurchaseView(View):
             # 获取交易的商品id
             product_id = payload.productId if payload and payload.productId else None
             if not product_id:
+                logger.info(f"苹果内购认证交易订单orderID:{order_id}, product_id获取失败")
                 pay_result_url = CommonService.get_payment_status_url(lang, 'fail')
                 return response.json(0, {'url': pay_result_url})
 
-            in_app_purchase_package_qs = InAppPurchasePackage.objects.filter(product_id=product_id, app_type=app_type)
+            in_app_purchase_package_qs = InAppPurchasePackage.objects.filter(product_id=product_id)
             if not in_app_purchase_package_qs.exists():
+                logger.info(f"苹果内购认证交易订单orderID:{order_id}, InAppPurchasePackage表未查询到product_id")
                 return response.json(173, "内购商品id不存在")
 
             # 验证订单是否存在
@@ -139,23 +159,68 @@ class InAppPurchaseView(View):
             if not store_qs.exists():
                 return response.json(173, "云存套餐不存在")
 
+            # 验证内购套餐是否存在
+            in_app_purchase_package_qs = InAppPurchasePackage.objects.filter(product_id=product_id)
+            if not in_app_purchase_package_qs.exists():
+                return response.json(173, "内购套餐不存在")
+
+            # 循环扣款
+            if original_transaction_identifier != "" and in_app_purchase_package_qs[0].package_type == 1:
+                device_apple_package_qs = DeviceApplePackage.objects.filter(
+                    original_transaction_id=original_transaction_identifier)
+                if device_apple_package_qs.exists():
+                    # 第一种情况: 套餐已过期再次订阅
+                    if device_apple_package_qs[0].uid == uid and device_apple_package_qs[0].subscription_status == 2:
+                        # 使用App Store服务器通知接口订阅
+                        pay_result_url = CommonService.get_payment_status_url(lang, 'success')
+                        return response.json(0, {'url': pay_result_url})
+
+                    # 第二种情况: 套餐未过期已取消再次订阅
+                    elif device_apple_package_qs[0].uid == uid and device_apple_package_qs[0].subscription_status == 3:
+                        # 使用App Store服务器通知接口修改订阅状态
+                        pay_result_url = CommonService.get_payment_status_url(lang, 'success')
+                        return response.json(0, {'url': pay_result_url})
+
+                    # 第三种情况: 首次订阅
+                    elif device_apple_package_qs[0].uid == uid and device_apple_package_qs[0].subscription_status == 0:
+                        logger.info(f"苹果内购认证交易订单orderID:{order_id}, 用户首次订阅")
+
+                    else:
+                        logger.info(
+                            f"错误调用此借口,orderID:{order_id}, uid:{uid}, 订阅状态:{device_apple_package_qs[0].subscription_status}")
+                        pay_result_url = CommonService.get_payment_status_url(lang, 'fail')
+                        return response.json(0, {'url': pay_result_url})
+
             # 设备开通云存
             now_time = int(time.time())
             uid_bucket_id = cls.enable_cloud(channel, now_time, order_id, store_qs, uid)
 
-            # 修改订单信息
-            order_qs.update(status=1, uid_bucket_id=uid_bucket_id, transaction_id=transaction_id, create_vod=1)
+            # 修改订阅状态
+            if payload.rawType == "Auto-Renewable Subscription":
+                original_transaction_id = payload.originalTransactionId
+                in_app_purchase_package = in_app_purchase_package_qs.values('id').first()
+                package_id = in_app_purchase_package['id']
+                DeviceApplePackage.objects.filter(userID=user_id, uid=uid, package_id=package_id).update(
+                    subscription_status=1, original_transaction_id=original_transaction_id,
+                )
+                order_qs.update(status=1, uid_bucket_id=uid_bucket_id,
+                                transaction_id=transaction_id, create_vod=1,
+                                payTime=now_time, updTime=now_time,
+                                original_transaction_id=original_transaction_id)
+            else:
+                order_qs.update(status=1, uid_bucket_id=uid_bucket_id,
+                                transaction_id=transaction_id, create_vod=1,
+                                payTime=now_time, updTime=now_time)
 
             # 构建云存套餐消息
             sys_msg_text_list = cls.cloud_storage_message(uid)
+
+            # 发送云存套餐购买消息
             cls.do_vod_msg_notice(uid, user_id, lang, sys_msg_text_list)
 
-            # 删除缓存
             redis_obj.del_data(redis_key)
             pay_result_url = CommonService.get_payment_status_url(lang, 'success')
-
             return response.json(0, {'url': pay_result_url})
-
         except Exception as e:
             redis_obj.del_data(redis_key)
             LOGGER.info('苹果内购认证交易接口异常:{}'.
@@ -286,65 +351,348 @@ class InAppPurchaseView(View):
         ali_sms.send_code_sms_cloud(phone=phone, params=params, sign_name=sign_ms, temp_msg=temp_msg)
 
     @classmethod
-    def app_store_server_notifications(cls, request):
+    def app_store_server_notifications(cls, request, request_dict):
         logger = logging.getLogger('apple_pay')
-        logger.info('App Store服务器通知请求类型:{}'.format(request.method))
-        logger.info('App Store服务器通知参数:{}'.format(request.POST))
-        logger.info('App Store服务器通知请求body:{}'.format(request.body))
-        payload = json.loads(request.body.decode('utf-8'))
-        logger.info('App Store服务器通知payload:{}'.format(payload))
-        # 获取 signedPayload
-        signed_payload = payload.get('signedPayload')
-        if not signed_payload:
+        if request.method != 'POST':
+            logger.info(f'App Store服务器通知不是post请求, 参数{request_dict}')
+            return HttpResponse(status=400)
+        try:
+            request_data = json.loads(request.body)
+        except json.JSONDecodeError:
+            logger.error('无法解析请求体为JSON')
             return HttpResponse(status=400)
 
-        bundle_id = 'com.ansjer.zccloud'
-        environment = ENV
-        root_certificates = []
-        for cert_name in [
-            'AppleIncRootCertificate.cer', 'AppleComputerRootCertificate.cer',
-            'AppleRootCA-G2.cer', 'AppleRootCA-G3.cer'
-        ]:
-            cert_path = '{}/Ansjer/file/in_app_purchase/{}'.format(BASE_DIR, cert_name)
-            with open(cert_path, 'rb') as file:
-                # 读取文件内容
-                root_certificates.append(file.read())
-
-        enable_online_checks = True
-        app_apple_id = 1355964934  # 生产环境必需
-
-        # 验证签名并解码 payload
-        verifier = SignedDataVerifier(
-            root_certificates, enable_online_checks, environment, bundle_id, app_apple_id)
-        decoded_payload = verifier.verify_and_decode_notification(signed_payload)
-        logger.info('App Store服务器通知decoded_payload: {}'.format(decoded_payload))
-        status_code = 200
-        if str(decoded_payload.rawNotificationType) == "REFUND":
-            # 一种通知类型,表示 App Store 成功退还了消耗性应用内购买、非消耗性应用内购买、自动续订或不可续订的交易。
-            # revocationDate 包含退款交易的时间戳。originalTransactionId 和 productId 用于标识原始交易和产品。revocationReason 包含原因。
-            # 要请求客户所有退款交易的列表,请参阅 App Store 服务器 API 中的获取退款历史记录。
-            # 1. 找套餐 使用 transaction_id 找orders
-            decoded_transaction_information = verifier.verify_and_decode_signed_transaction(
-                decoded_payload.data.signedTransactionInfo)
-            transaction_id = decoded_transaction_information.transactionId
-            logger.info('App Store服务器通知退款, transaction_id:{}'.format(transaction_id))
-            orders_qs = Order_Model.objects.filter(transaction_id=transaction_id)
-            # 2. 查找云存套餐使用表 和 云存套餐
-            if orders_qs.exists():
-                orders_qs.update(status=11)
+        request_data['bundleId'] = 'com.ansjer.zccloud'
+        updated_request_body = json.dumps(request_data)
+
+        if CONFIG_INFO == CONFIG_TEST:
+            logger.info('测试环境, App Store服务器通知发送到测试服')
+            response_test = requests.post(url="https://test.zositechc.cn/inAppPurchase/serverNotifications",
+                                          json=updated_request_body)
+            return HttpResponse(status=response_test.status_code)
+        response_us = requests.post(url="https://www.dvema.com/inAppPurchase/serverNotifications",
+                                    json=updated_request_body)
+        status_code = response_us.status_code
+        if status_code != 200:
+            response_eu = requests.post(url="https://api.zositeche.com/inAppPurchase/serverNotifications",
+                                        json=updated_request_body)
+            status_code = response_eu.status_code
+        if status_code == 200:
+            return HttpResponse(status=200)
+        return HttpResponse(status=400)
+
+    @classmethod
+    def server_notifications(cls, request):
+        try:
+            logger.info('App Store服务器通知请求类型:{}'.format(request.method))
+            logger.info('App Store服务器通知参数:{}'.format(request.POST))
+            logger.info('App Store服务器通知请求body:{}'.format(request.body))
+            payload = json.loads(request.body.decode('utf-8'))
+            logger.info('App Store服务器通知payload:{}'.format(payload))
+            # 获取 signedPayload
+            signed_payload = payload.get('signedPayload')
+            bundle_id = payload.get('bundleId')
+            if not signed_payload:
+                return HttpResponse(status=400)
+            if bundle_id == "com.ansjer.zccloud":
+                app_type = 1
+            else:
+                app_type = 2
+
+            in_app_purchase_obj = InAppPurchase(bundle_id=bundle_id)
+            # AppStoreServerAPIClient 用于查询交易信息
+            client = in_app_purchase_obj.client
+            # SignedDataVerifier 用于解析查询到的交易信息
+            signed_data_verifier = in_app_purchase_obj.verifier
+
+            # 验证签名并解码 payload
+            decoded_payload = signed_data_verifier.verify_and_decode_notification(signed_payload)
+
+            logger.info(f"App Store服务器通知解码后decoded_payload:{decoded_payload}")
+            raw_notification_type = str(decoded_payload.rawNotificationType)
+            raw_subtype = str(decoded_payload.rawSubtype)
+            logger.info(f"App Store服务器通知, 大类型{raw_notification_type}, 小类型{raw_subtype}")
+
+            if str(decoded_payload.rawNotificationType) == "DID_RENEW":
+                # 续订
+                decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                    decoded_payload.data.signedTransactionInfo)
+                # originalTransactionId 原始购买的交易标识符
+                original_transaction_id = decoded_transaction_information.originalTransactionId
+                transaction_id = decoded_transaction_information.transactionId
+                logger.info(f"App Store服务器通知, 续订originalTransactionId{original_transaction_id}")
+                if not original_transaction_id:
+                    logger.info(f"App Store服务器通知 originalTransactionId原始购买的交易标识符为空, 返回状态 400")
+                    return HttpResponse(status=400)
+                ord_order = Order_Model.objects.filter(original_transaction_id=original_transaction_id).order_by(
+                    '-addTime').values("channel", "UID", "payType", "userID_id", "rank_id")
+                if not ord_order.exists():
+                    logger.info(
+                        f"App Store服务器通知, 未查询到旧订单信息, originalTransactionId:{original_transaction_id}, 返回状态 400")
+                    return HttpResponse(status=400)
+
+                channel = ord_order[0]["channel"]
+                uid = ord_order[0]["UID"]
+                pay_type = ord_order[0]["payType"]
+                user_id = ord_order[0]["userID_id"]
+
+                store_qs = Store_Meal.objects.filter(id=ord_order[0]["rank_id"]). \
+                    values(
+                    'id', 'currency', 'price', 'lang__content', 'day', 'commodity_type', 'lang__title',
+                    'expire', 'lang__lang',
+                    'commodity_code', 'discount_price', 'bucket_id', 'bucket__mold', 'cycle_config_id', 'is_ai')
+
+                if not store_qs.exists():
+                    logger.info(f"App Store服务器通知云存套餐不存在, 返回状态 400")
+                    return HttpResponse(status=400)
+
+                # 解决云存充值成功, 由于一些原因返回500 导致苹果未扣款的问题
+                if Order_Model.objects.filter(transaction_id=transaction_id, status=1).exists():
+                    logger.info(f"App Store服务器通知云存续订订单已存在, transactionId:{transaction_id} 返回状态 200")
+                    return HttpResponse(status=200)
+
+                order_id = CommonService.createOrderID()
+                rank_id = store_qs[0]['id']
+                currency = store_qs[0]['currency']
+                price = store_qs[0]['price']
+                is_ai = store_qs[0]['is_ai']
+                content = store_qs[0]['lang__content']
+                commodity_code = store_qs[0]['commodity_code']
+                commodity_type = store_qs[0]['commodity_type']
+                lang = store_qs[0]['lang__lang']
+                order_type = 1 if is_ai else 0
+                now_time = int(time.time())
+
+                store_meal_qs = Store_Meal.objects.filter(id=rank_id, lang__lang='cn', is_show=0). \
+                    values('lang__title', 'lang__content')
+                if store_meal_qs.exists():
+                    store_meal_name = store_meal_qs[0]['lang__title'] + '-' + store_meal_qs[0]['lang__content']
+                else:
+                    store_meal_name = '未知套餐'
+
+                # 创建订单
+                order = Order_Model.objects.create(
+                    orderID=order_id, UID=uid, channel=channel, userID_id=user_id, desc=content,
+                    payType=pay_type, payTime=now_time, price=price, currency=currency, addTime=now_time,
+                    updTime=now_time, order_type=order_type, commodity_code=commodity_code,
+                    commodity_type=commodity_type, rank_id=rank_id, ai_rank_id=1, status=1,
+                    store_meal_name=store_meal_name, app_type=app_type
+                )
+
+                # 充值云存套餐
+                uid_bucket_id = cls.enable_cloud(channel, now_time, order_id, store_qs, uid)
+
+                # 修改订单信息
+                order.uid_bucket_id = uid_bucket_id
+                order.transaction_id = transaction_id
+                order.original_transaction_id = original_transaction_id
+                order.save()
+
+                # 构建云存套餐消息
+                sys_msg_text_list = cls.cloud_storage_message(uid)
+                cls.do_vod_msg_notice(uid, user_id, lang, sys_msg_text_list)
+                return HttpResponse(status=200)
+
+            elif str(decoded_payload.rawNotificationType) == "SUBSCRIBED":
+                # 处理订阅 ---> 首次充值逻辑写在了认证交易
+                if decoded_payload.rawSubtype == "RESUBSCRIBE":
+                    decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                        decoded_payload.data.signedTransactionInfo)
+                    # originalTransactionId 原始购买的交易标识符
+                    original_transaction_id = decoded_transaction_information.originalTransactionId
+                    transaction_id = decoded_transaction_information.transactionId
+                    logger.info(
+                        f"App Store服务器通知,再次订阅originalTransactionId原始购买的交易标识符{original_transaction_id}")
+                    if not original_transaction_id:
+                        logger.info(f"App Store服务器通知 originalTransactionId原始购买的交易标识符为空, 返回状态 400")
+                        return HttpResponse(status=400)
+                    # 查旧订单消息
+                    ord_order_qs = Order_Model.objects.filter(original_transaction_id=original_transaction_id)
+                    if not ord_order_qs.exists():
+                        logger.info(
+                            f"App Store服务器通知未查询到旧订单信息, originalTransactionId:{original_transaction_id}, 返回状态 400")
+                        return HttpResponse(status=400)
+
+                    # 解决云存充值成功, 由于一些原因返回500 导致苹果未扣款的问题
+                    if Order_Model.objects.filter(transaction_id=transaction_id, status=1).exists():
+                        logger.info(
+                            f"App Store服务器通知云存续订订单已存在, transactionId:{transaction_id} 返回状态 200")
+                        return HttpResponse(status=200)
+
+                    ord_order = ord_order_qs.order_by('-addTime').values("channel", "UID", "payType", "userID_id")
+                    channel = ord_order[0]["channel"]
+                    uid = ord_order[0]["UID"]
+                    pay_type = ord_order[0]["payType"]
+                    user_id = ord_order[0]["userID_id"]
+                    # 用产品id找到使用的套餐
+                    product_id = decoded_transaction_information.productId
+                    rank_id = InAppPurchasePackage.objects.filter(product_id=product_id).values("rank")[0]["rank"]
+                    store_qs = Store_Meal.objects.filter(id=rank_id). \
+                        values(
+                        'id', 'currency', 'price', 'lang__content', 'day', 'commodity_type', 'lang__title',
+                        'expire', 'lang__lang',
+                        'commodity_code', 'discount_price', 'bucket_id', 'bucket__mold', 'cycle_config_id', 'is_ai')
+
+                    if not store_qs.exists():
+                        logger.info(f"App Store服务器通知云存套餐不存在, 返回状态 400")
+                        return HttpResponse(status=400)
+
+                    order_id = CommonService.createOrderID()
+                    rank_id = store_qs[0]['id']
+                    currency = store_qs[0]['currency']
+                    price = store_qs[0]['price']
+                    is_ai = store_qs[0]['is_ai']
+                    content = store_qs[0]['lang__content']
+                    commodity_code = store_qs[0]['commodity_code']
+                    commodity_type = store_qs[0]['commodity_type']
+                    lang = store_qs[0]['lang__lang']
+                    order_type = 1 if is_ai else 0
+                    now_time = int(time.time())
+
+                    store_meal_qs = Store_Meal.objects.filter(id=rank_id, lang__lang='cn', is_show=0). \
+                        values('lang__title', 'lang__content')
+                    if store_meal_qs.exists():
+                        store_meal_name = store_meal_qs[0]['lang__title'] + '-' + store_meal_qs[0]['lang__content']
+                    else:
+                        store_meal_name = '未知套餐'
+
+                    order = Order_Model.objects.create(
+                        orderID=order_id, UID=uid, channel=channel, userID_id=user_id, desc=content,
+                        payType=pay_type, payTime=now_time, price=price, currency=currency, addTime=now_time,
+                        updTime=now_time, order_type=order_type, commodity_code=commodity_code,
+                        commodity_type=commodity_type, rank_id=rank_id, ai_rank_id=1, status=1,
+                        store_meal_name=store_meal_name, app_type=app_type
+                    )
+
+                    # 充值云存套餐
+                    uid_bucket_id = cls.enable_cloud(channel, now_time, order_id, store_qs, uid)
+
+                    # 修改订单信息
+                    order.uid_bucket_id = uid_bucket_id
+                    order.transaction_id = transaction_id
+                    order.original_transaction_id = original_transaction_id
+                    order.save()
+
+                    DeviceApplePackage.objects.filter(userID=user_id, uid=uid).update(subscription_status=1,
+                                                                                      update_time=int(time.time()))
+
+                    # 构建云存套餐消息
+                    sys_msg_text_list = cls.cloud_storage_message(uid)
+
+                    cls.do_vod_msg_notice(uid, user_id, lang, sys_msg_text_list)
+                return HttpResponse(status=200)
+
+            elif str(decoded_payload.rawNotificationType) == "EXPIRED":
+                # 一种通知类型,与其子类型一起表示订阅已过期。如果subtype为
+                # VOLUNTARY(自愿),则表示订阅在用户禁用订阅续订后过期。如果subtype是
+                # BILLING_RETRY(计费重试),则表示订阅过期,因为计费重试期结束时没有成功的计费交易。如果subtype为
+                # PRICE_INCREASE,则表示订阅已过期,因为客户不同意需要客户同意的价格上涨。如果subtype为
+                # PRODUCT_NOT_FOR_SALE,则表示订阅已过期,因为在订阅尝试续订时,产品已不可购买。
+                # 没有子类型的通知表示订阅因其他原因过期。
+                decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                    decoded_payload.data.signedTransactionInfo)
+                # originalTransactionId 原始购买的交易标识符
+                original_transaction_id = decoded_transaction_information.originalTransactionId
+                if not original_transaction_id:
+                    logger.info(f"App Store服务器通知 originalTransactionId原始购买的交易标识符为空, 返回状态 400")
+                    return HttpResponse(status=400)
+                device_apple_package_qs = DeviceApplePackage.objects.filter(
+                    original_transaction_id=original_transaction_id)
+                if not device_apple_package_qs.exists():
+                    return HttpResponse(status=400)
+                device_apple_package_qs.update(subscription_status=2, update_time=int(time.time()))
+
+            elif str(decoded_payload.rawNotificationType) == "CONSUMPTION_REQUEST":
+                # 一种通知类型,指示客户发起了消费型 App 内购买项目或自动续期订阅的退款请求,并且 App Store 要求您提供消费数据。有关详细信息,请参阅发送消耗信息。
+                decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                    decoded_payload.data.signedTransactionInfo)
+                transaction_id = decoded_transaction_information.transactionId
+                orders_qs = Order_Model.objects.filter(transaction_id=transaction_id)
+                if not orders_qs.exists():
+                    return HttpResponse(status=400)
+                orderID = orders_qs[0].orderID
+                unused_uid_meal_qs = Unused_Uid_Meal.objects.filter(order_id=orderID)
+                uid_bucket_qs = UID_Bucket.objects.filter(orderId=orderID, endTime__gt=int(time.time()))
+                if unused_uid_meal_qs.exists():
+                    consumptionStatus = ConsumptionStatus.NOT_CONSUMED
+                    deliveryStatus = DeliveryStatus.DELIVERED_AND_WORKING_PROPERLY
+                elif uid_bucket_qs.exists():
+                    consumptionStatus = ConsumptionStatus.PARTIALLY_CONSUMED
+                    deliveryStatus = DeliveryStatus.DELIVERED_AND_WORKING_PROPERLY
+                elif UID_Bucket.objects.filter(orderId=orderID, endTime__lt=int(time.time())):
+                    consumptionStatus = ConsumptionStatus.FULLY_CONSUMED
+                    deliveryStatus = DeliveryStatus.DELIVERED_AND_WORKING_PROPERLY
+                else:
+                    consumptionStatus = ConsumptionStatus.UNDECLARED
+                    deliveryStatus = DeliveryStatus.DID_NOT_DELIVER_FOR_OTHER_REASON
+                in_app_refund_qs = InAppRefund.objects.filter(transaction_id=transaction_id).exists()
+                refundPreference = RefundPreference.PREFER_DECLINE
+                if in_app_refund_qs.exists():
+                    if in_app_refund_qs[0].refund_preference == 1:
+                        refundPreference = RefundPreference.PREFER_GRANT
+                consumption_request = ConsumptionRequest(
+                    customerConsented=True,
+                    consumptionStatus=consumptionStatus,
+                    platform=Platform.UNDECLARED,
+                    sampleContentProvided=True,
+                    deliveryStatus=deliveryStatus,
+                    appAccountToken="",
+                    accountTenure=AccountTenure.UNDECLARED,
+                    playTime=PlayTime.UNDECLARED,
+                    lifetimeDollarsRefunded=LifetimeDollarsRefunded.UNDECLARED,
+                    lifetimeDollarsPurchased=LifetimeDollarsPurchased.UNDECLARED,
+                    userStatus=UserStatus.ACTIVE,
+                    refundPreference=refundPreference,
+                )
+                client.send_consumption_data(transaction_id, consumption_request)
+                return HttpResponse(status=200)
+
+            elif str(decoded_payload.rawNotificationType) == "DID_CHANGE_RENEWAL_STATUS":
+                decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                    decoded_payload.data.signedTransactionInfo)
+                original_transaction_id = decoded_transaction_information.originalTransactionId
+                if not original_transaction_id:
+                    logger.info(f"App Store服务器通知 originalTransactionId原始购买的交易标识符为空, 返回状态 400")
+                    return HttpResponse(status=400)
+                if decoded_payload.rawSubtype == "AUTO_RENEW_ENABLED":
+                    # 自动续订被开启
+                    device_apple_package_qs = DeviceApplePackage.objects.filter(
+                        original_transaction_id=original_transaction_id)
+                    if not device_apple_package_qs.exists():
+                        return HttpResponse(status=400)
+                    device_apple_package_qs.update(subscription_status=1, update_time=int(time.time()))
+                else:
+                    # 自动续订被禁用
+                    device_apple_package_qs = DeviceApplePackage.objects.filter(
+                        original_transaction_id=original_transaction_id)
+                    if not device_apple_package_qs.exists():
+                        return HttpResponse(status=400)
+                    device_apple_package_qs.update(subscription_status=3, update_time=int(time.time()))
+
+            elif str(decoded_payload.rawNotificationType) == "REFUND":
+                # 一种通知类型,表示 App Store 成功退还了消耗性应用内购买、非消耗性应用内购买、自动续订或不可续订的交易。
+                # revocationDate 包含退款交易的时间戳。originalTransactionId 和 productId 用于标识原始交易和产品。revocationReason 包含原因。
+                # 要请求客户所有退款交易的列表,请参阅 App Store 服务器 API 中的获取退款历史记录。
+                decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                    decoded_payload.data.signedTransactionInfo)
+                transaction_id = decoded_transaction_information.transactionId
+                logger.info('App Store服务器通知退款, transaction_id:{}'.format(transaction_id))
+                orders_qs = Order_Model.objects.filter(transaction_id=transaction_id)
+                if not orders_qs.exists():
+                    return HttpResponse(status=400)
+                orders_qs.update(status=5, updTime=int(time.time()))
                 orderID = orders_qs[0].orderID
                 uid = orders_qs[0].UID
-                user_id = orders_qs[0].userID
-                # 3. 未使用则删除未使用套餐表,已使用过则删除设备正在使用套餐,并关闭设备云存
                 uid_bucket_qs = UID_Bucket.objects.filter(uid=uid, orderId=orderID, use_status=1,
                                                           endTime__gt=int(time.time()))
                 unused_uid_meal_qs = Unused_Uid_Meal.objects.filter(order_id=orderID)
-                ai_service_qs = AiService.objects.filter(uid=uid, orderId=orderID, use_status=1,
+                ai_service_qs = AiService.objects.filter(uid=uid, orders=orderID, use_status=1,
                                                          endTime__gt=int(time.time()))
                 if unused_uid_meal_qs.exists():
                     unused_uid_meal_qs.delete()
                 if uid_bucket_qs.exists():
-                    uid_bucket_qs.update(status=0, use_status=2, endTime=int(time.time()), updateTime=int(time.time()))
+                    uid_bucket_qs.update(status=0, use_status=2, endTime=int(time.time()),
+                                         updateTime=int(time.time()))
                 if ai_service_qs.exists():
                     ai_service_qs.update(detect_status=0, use_status=2, endTime=int(time.time()),
                                          updTime=int(time.time()))
@@ -353,18 +701,57 @@ class InAppPurchaseView(View):
                     thing_name = CommonService.query_serial_with_uid(uid)  # 存在序列号则为使用序列号作为物品名
                     topic_name = 'ansjer/generic/{}'.format(thing_name)
                     req_success = CommonService.req_publish_mqtt_msg(thing_name, topic_name, msg)
-                    LOGGER.info(f'App Store服务器通知用户退款, 关闭AI:{req_success}')
+                    logger.info(f'App Store服务器通知用户退款, 关闭AI:{req_success}')
+                return HttpResponse(status=200)
+
+            elif str(decoded_payload.rawNotificationType) == "REFUND_DECLINED":
+                # 一种通知类型,表示 App Store 由于客户提出的争议而撤销了先前批准的退款。如果您的应用程序因相关退款而撤销了内容或服务,则需要恢复这些内容或服务。
+                # 此通知类型可适用于任何应用程序内购买类型:消耗品、非消耗品、不可续订订阅和自动续订订阅。对于自动续订,当 App Store 撤销退款时,续订日期保持不变。
+                decoded_transaction_information = signed_data_verifier.verify_and_decode_signed_transaction(
+                    decoded_payload.data.signedTransactionInfo)
+                transaction_id = decoded_transaction_information.transactionId
+                logger.info(
+                    'App Store服务器通知,撤销了批准的退款,不恢复套餐,手动处理 transaction_id:{}'.format(transaction_id))
 
-                # 4.发送邮件告知用户退款
-                email_content = f'{CONFIG_INFO}用户{user_id}, 订单:{orderID}, 设备{uid}退款'
-                S3Email().faEmail(email_content, 'servers@ansjer.com')
             else:
-                if CONFIG_INFO == CONFIG_US:
-                    url = "https://api.zositeche.com/inAppPurchase/AppStoreServerNotifications"
-                    eur_response = requests.post(url=url, json=json.loads(request.body))
-                    status_code = eur_response.status_code
+                logger.info(f"App Store服务器通知decoded_payload.rawNotificationType 未处理")
+                return HttpResponse(status=500)
 
-        return HttpResponse(status=status_code)
+        except Exception as e:
+            logger.info('App Store服务器通知异常:{}'.
+                        format('error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e))))
+            return HttpResponse(status=500)
+
+    @classmethod
+    def app_store_server_notifications_vsees(cls, request, request_dict):
+        logger = logging.getLogger('apple_pay')
+        if request.method != 'POST':
+            logger.info(f'App Store服务器通知不是post请求, 参数{request_dict}')
+            return HttpResponse(status=400)
+        try:
+            request_data = json.loads(request.body)
+        except json.JSONDecodeError:
+            logger.error('无法解析请求体为JSON')
+            return HttpResponse(status=400)
+
+        request_data['bundleId'] = 'com.ansjer.zccloud'
+        updated_request_body = json.dumps(request_data)
+
+        if CONFIG_INFO == CONFIG_TEST:
+            logger.info('测试环境, App Store服务器通知发送到测试服')
+            response_test = requests.post(url="https://test.zositechc.cn/inAppPurchase/serverNotifications",
+                                          json=updated_request_body)
+            return HttpResponse(status=response_test.status_code)
+        response_us = requests.post(url="https://www.dvema.com/inAppPurchase/serverNotifications",
+                                    json=updated_request_body)
+        status_code = response_us.status_code
+        if status_code != 200:
+            response_eu = requests.post(url="https://api.zositeche.com/inAppPurchase/serverNotifications",
+                                        json=updated_request_body)
+            status_code = response_eu.status_code
+        if status_code == 200:
+            return HttpResponse(status=200)
+        return HttpResponse(status=400)
 
     @classmethod
     def vsees_notifications(cls, request):

+ 15 - 1
Model/models.py

@@ -5170,7 +5170,7 @@ class InAppPurchasePackage(models.Model):
 
     class Meta:
         db_table = 'in_app_purchase_package'
-        verbose_name = '苹果内购套餐表'
+        verbose_name = '苹果内购订阅套餐表'
 
 
 class DeviceApplePackage(models.Model):
@@ -5316,6 +5316,20 @@ class UserSetStatus(models.Model):
         verbose_name = '用户设置状态'
 
 
+class InAppRefund(models.Model):
+    id = models.AutoField(primary_key=True, verbose_name='自增标记ID')
+    transaction_id = models.CharField(default='', max_length=32, verbose_name='苹果事务id')
+    orderID = models.CharField(blank=True, max_length=20, verbose_name=u'订单id')
+    # 0不同意 1同意
+    refund_preference = models.IntegerField(default=0, verbose_name='是否同意退款')
+    created_time = models.IntegerField(default=0, verbose_name='创建时间')
+    updated_time = models.IntegerField(default=0, verbose_name='更新时间')
+
+    class Meta:
+        db_table = 'in_app_refund'
+        verbose_name = '退款状态'
+
+
 class VodHlsBase(models.Model):
     id = models.AutoField(primary_key=True, verbose_name='主键')
     uid = models.CharField(max_length=20, db_index=True, verbose_name='uid')

+ 6 - 2
Object/AppleInAppPurchaseSubscriptionObject.py

@@ -1,7 +1,11 @@
+# @Author    : linhaohong
+# @File      : AppleInAppPurchaseSubscriptionObject.py
+# @Time      : 2024/9/4 11:58
+from appstoreserverlibrary.receipt_utility import ReceiptUtility
+
 from Ansjer.config import LOGGER, CONFIG_INFO, CONFIG_TEST, BASE_DIR, IN_APP_CONFIG
 from appstoreserverlibrary.api_client import AppStoreServerAPIClient
 from appstoreserverlibrary.models.Environment import Environment
-from appstoreserverlibrary.receipt_utility import ReceiptUtility
 from appstoreserverlibrary.signed_data_verifier import SignedDataVerifier
 
 from Model.models import Order_Model, DeviceApplePackage
@@ -27,7 +31,7 @@ class InAppConfig:
 
 
 class InAppPurchase:
-    def __init__(self, bundle_id):
+    def __init__(self, bundle_id="com.ansjer.zccloud"):
         """
         初始化 InAppSubscription,加载私钥并初始化客户端和解码器。
         :param app_config: 包含内购相关配置的 AppConfig 实例。