diff --git a/MyApi/API_Log.py b/MyApi/API_Log.py new file mode 100644 index 0000000..df5e126 --- /dev/null +++ b/MyApi/API_Log.py @@ -0,0 +1,23 @@ +import logging +from logging.handlers import TimedRotatingFileHandler + + +# 日志配置 +def get_logger(name, log_file, when='midnight', backup_count=7): + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + + # 创建TimedRotatingFileHandler + handler = TimedRotatingFileHandler( + log_file, + when=when, # 按天切割 + interval=1, # 每1天切割一次 + backupCount=backup_count # 保留7个备份 + ) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + + logger.addHandler(handler) + return logger + + diff --git a/MyApi/AiMssage.py b/MyApi/AiMssage.py new file mode 100644 index 0000000..99bc313 --- /dev/null +++ b/MyApi/AiMssage.py @@ -0,0 +1,190 @@ +from django.shortcuts import render +import json +from http import HTTPStatus +from dashscope import Application +from django.http import JsonResponse +from .models import User, ChatRecord +from django.utils.crypto import get_random_string +from Crypto.Cipher import AES +import base64 +from .views import update_usage_count + +def decrypt_param(x, t): + # 创建AES解密器对象 + key = b'qw5w6SFE2D1jmxyd' + iv = b'345GDFED433223DF' + + # 检查 x 和 t 是否为空 + if not x or not t: + return False + + try: + cipher = AES.new(key, AES.MODE_CBC, iv) + # 将密文进行Base64解码 + ciphertext_bytes = base64.b64decode(x) + # 使用解密器解密密文 + plaintext_bytes = cipher.decrypt(ciphertext_bytes) + # 删除填充字符 + plaintext = plaintext_bytes.rstrip(b'\0').decode('utf-8') + # 比较解密后的明文和 t + if plaintext.rstrip('\x03') == t.rstrip('\x03'): + return True + else: + return False + except Exception as e: + print(f"解密过程出现错误: {e}") + return False + +def call_bailian_app(request): + body = request.body.decode("utf-8") + data = json.loads(body) + user_id = data.get("user_id") + openid = data.get("openid") + role_name = data.get("role", "AI助手") # 默认角色为“默认AI聊天” + prompt = data.get("prompt") + x = request.headers.get('X', '') + t = request.headers.get('T', '') + increment = -1 + function_type = 'call_bailian_app' + result = update_usage_count(openid, increment, function_type) + if decrypt_param(x, t): + if result['success']: + print(f"收到请求: user_id={user_id}, role_name={role_name}, prompt={prompt}") + try: + user = User.objects.get(nickname=user_id) + print(f"找到用户: {user.nickname}") + except User.DoesNotExist: + print("用户不存在") + return JsonResponse({"message": "用户不存在"}, status=404) + + # 检查是否已有该用户和角色的会话 + conversation = ChatRecord.objects.filter(nickname=user.nickname, role=role_name).first() + if not conversation: + print(f"新会话ID") + response = Application.call( + app_id=get_app_id(role_name), + prompt=prompt, + api_key="sk-9458cae1bed7460c9780e52ca6005cae", + ) + conversation_id = response.output.get('session_id') + else: + conversation_id = conversation.conversation_id + response = Application.call( + app_id=get_app_id(role_name), + prompt=prompt, + api_key="sk-9458cae1bed7460c9780e52ca6005cae", + session_id=conversation_id + ) + print(f"已有会话ID: {conversation_id}") + + print(f"API调用响应状态: {response.status_code}") + + if response.status_code != HTTPStatus.OK: + print(f"API调用失败: request_id={response.request_id}, message={response.message}") + return JsonResponse({ + "message": "调用应用程序失败", + "request_id": response.request_id, + "code": response.status_code, + "details": response.message + }, status=response.status_code) + + ai_response = response.output.get('text') if response.output else "AI无回复" + print(f"AI回复: {ai_response}") + + ChatRecord.objects.create( + openid=user.openid, + nickname=user.nickname, + conversation_id=conversation_id, + role=role_name, + message_content=prompt, + is_response=False + ) + + # 保存AI回复 + ChatRecord.objects.create( + openid=user.openid, + nickname=user.nickname, + conversation_id=conversation_id, + role=role_name, + message_content=ai_response, + is_response=True + ) + + # 检索会话的最近10条消息 + last_messages = ChatRecord.objects.filter(nickname=user.nickname, conversation_id=conversation_id).order_by('-timestamp')[:10] + last_messages_list = [ + { + "content": msg.message_content, + "is_response": msg.is_response, + "role":msg.role, + "timestamp": msg.timestamp.strftime('%Y-%m-%d %H:%M:%S') + } for msg in reversed(last_messages) + ] + + print(f"最近10条消息: {last_messages_list}") + + return JsonResponse({ + "message": "成功", + "ai_response": ai_response, + "conversation_id": conversation_id, + "last_messages": last_messages_list + }, status=HTTPStatus.OK) + else: + return JsonResponse(result) + else: + return JsonResponse({'error': '非法参数'}, status=400) + +# 新增接口:获取用户和角色的最近10条聊天记录 +def get_recent_chat_records(request): + body = request.body.decode("utf-8") + data = json.loads(body) + user_id = data.get("user_id") + role_name = data.get("role", "AI助手") # 默认角色为“默认AI聊天” + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + print(f"收到请求: user_id={user_id}, role_name={role_name}") + + try: + user = User.objects.get(nickname=user_id) + print(f"找到用户: {user.nickname}") + except User.DoesNotExist: + print("用户不存在") + return JsonResponse({"message": "用户不存在"}, status=404) + + # 获取该用户和角色的最近10条聊天记录 + last_messages = ChatRecord.objects.filter(nickname=user.nickname, role=role_name).order_by('-timestamp')[:10] + last_messages_list = [ + { + "content": msg.message_content, + "is_response": msg.is_response, + "role": msg.role, + "timestamp": msg.timestamp.strftime('%Y-%m-%d %H:%M:%S') + } for msg in reversed(last_messages) + ] + + print(f"最近10条消息: {last_messages_list}") + + return JsonResponse({ + "message": "成功", + "last_messages": last_messages_list + }, status=HTTPStatus.OK) + else: + return JsonResponse({'error': '非法参数'}, status=400) +def get_app_id(role_name): + role_app_id_map = { + "法律咨询": "446cde34829e4d6490667b1271dee537", + "行业分析师": "bdce7fba11b147ca846a1b4d1f5e7e72", + "写工作报告": "f340613da5574815975acdc10bac4e47", + "产品顾问": "f9b0612793e94a669fe7ad2fecbda50e", + "写爆款文案": "2c8d6675a9794711a886a5e8cf714afe", + "写公众号文章": "4fb69f6bd9bb49c9a6329202c6f28c84", + "文案改写": "b94734c09c414814bc0505a4e5b636c6", + "招聘助理": "c70caf3fd67b44c4947df70f05cf9f10", + "AI助手": "92d23cd2c5d64ef5ac6ad02692fc510b", + "小红书文案": "c5211023cbf64d548ea55a47c516007a", + "写广告文案": "6c9770da90ec47979ce899cec49c396c", + } + + return role_app_id_map.get(role_name, "app_id_default") + diff --git a/MyApi/WXBizDataCrypt.py b/MyApi/WXBizDataCrypt.py new file mode 100644 index 0000000..720209c --- /dev/null +++ b/MyApi/WXBizDataCrypt.py @@ -0,0 +1,22 @@ +from base64 import b64decode +from Crypto.Cipher import AES +import json + +class WXBizDataCrypt: + def __init__(self, appid, session_key): + self.appid = appid + self.session_key = b64decode(session_key) + + def decrypt(self, encrypted_data, iv): + # AES解密 + try: + cipher = AES.new(self.session_key, AES.MODE_CBC, b64decode(iv)) + decrypted = json.loads(self._unpad(cipher.decrypt(b64decode(encrypted_data)))) + if decrypted['watermark']['appid'] != self.appid: + raise Exception('Invalid Buffer') + return decrypted + except Exception as e: + raise Exception('Failed to decrypt data') + + def _unpad(self, s): + return s[:-ord(s[len(s)-1:])] \ No newline at end of file diff --git a/MyApi/__init__.py b/MyApi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MyApi/admin.py b/MyApi/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/MyApi/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/MyApi/apps.py b/MyApi/apps.py new file mode 100644 index 0000000..041ee5d --- /dev/null +++ b/MyApi/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MyapiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'MyApi' diff --git a/MyApi/demo.mp3 b/MyApi/demo.mp3 new file mode 100644 index 0000000..8cc7a0a Binary files /dev/null and b/MyApi/demo.mp3 differ diff --git a/MyApi/demo.mp4 b/MyApi/demo.mp4 new file mode 100644 index 0000000..b30fb16 Binary files /dev/null and b/MyApi/demo.mp4 differ diff --git a/MyApi/demo.mp4.mp3 b/MyApi/demo.mp4.mp3 new file mode 100644 index 0000000..8cc7a0a Binary files /dev/null and b/MyApi/demo.mp4.mp3 differ diff --git a/MyApi/migrations/0001_initial.py b/MyApi/migrations/0001_initial.py new file mode 100644 index 0000000..219afd5 --- /dev/null +++ b/MyApi/migrations/0001_initial.py @@ -0,0 +1,615 @@ +# Generated by Django 5.0.3 on 2024-06-01 04:23 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Administrator", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("username", models.CharField(max_length=150, unique=True)), + ("password", models.CharField(max_length=128)), + ("role", models.CharField(max_length=100)), + ("is_active", models.BooleanField(default=True)), + ("last_login", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "Administrator", + }, + ), + migrations.CreateModel( + name="ApiCallLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "source", + models.CharField( + blank=True, + choices=[("index", "Index"), ("weixin", "Weixin")], + max_length=100, + null=True, + verbose_name="来源", + ), + ), + ( + "openid", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="openid" + ), + ), + ( + "nickname", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="用户标识" + ), + ), + ( + "wxid", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="微信id" + ), + ), + ( + "wechat_alias", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="微信号" + ), + ), + ("api_name", models.CharField(max_length=255, verbose_name="调用接口")), + ( + "is_successful", + models.BooleanField(default=False, verbose_name="是否成功"), + ), + ("remarks", models.TextField(blank=True, null=True, verbose_name="备注")), + ( + "call_time", + models.DateTimeField(auto_now_add=True, verbose_name="调用时间"), + ), + ], + options={ + "verbose_name": "API调用日志", + "verbose_name_plural": "API调用日志", + "db_table": "api_call_log", + }, + ), + migrations.CreateModel( + name="ChatRecord", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("openid", models.CharField(default=None, max_length=150)), + ("nickname", models.CharField(max_length=150, verbose_name="用户昵称")), + ( + "conversation_id", + models.CharField(max_length=100, verbose_name="会话ID"), + ), + ("role", models.CharField(max_length=100, verbose_name="角色")), + ("message_content", models.TextField(verbose_name="消息内容")), + ( + "is_response", + models.BooleanField(default=False, verbose_name="是否为AI回复"), + ), + ( + "timestamp", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="时间戳" + ), + ), + ], + options={ + "verbose_name": "聊天记录", + "verbose_name_plural": "聊天记录", + "db_table": "chat_record", + "ordering": ["-timestamp"], + }, + ), + migrations.CreateModel( + name="Copywriting", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "openid", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="openid" + ), + ), + ( + "nickname", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="用户标识" + ), + ), + ("text_content", models.TextField(verbose_name="文本内容")), + ("source", models.CharField(max_length=255, verbose_name="来源")), + ( + "tag", + models.CharField(default=None, max_length=255, verbose_name="标签"), + ), + ("popularity", models.IntegerField(default=0, verbose_name="热度")), + ( + "added_time", + models.DateTimeField(auto_now_add=True, verbose_name="添加时间"), + ), + ( + "is_approved", + models.BooleanField(default=False, verbose_name="审核通过"), + ), + ], + options={ + "verbose_name": "文案库", + "verbose_name_plural": "文案库", + "db_table": "Copywriting", + }, + ), + migrations.CreateModel( + name="FriendRequest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("fromusername", models.CharField(max_length=255, verbose_name="微信ID")), + ( + "alias", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="微信号" + ), + ), + ( + "fromnickname", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="昵称" + ), + ), + ( + "country", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="国家" + ), + ), + ( + "province", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="省份" + ), + ), + ( + "city", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="城市" + ), + ), + ( + "sex", + models.CharField( + blank=True, max_length=1, null=True, verbose_name="性别" + ), + ), + ( + "scene", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="来源" + ), + ), + ("time", models.DateTimeField(verbose_name="申请时间")), + ("towxid", models.CharField(max_length=255, verbose_name="目标微信ID")), + ], + options={ + "verbose_name": "添加好友请求", + "verbose_name_plural": "添加好友请求", + "db_table": "FriendRequest", + }, + ), + migrations.CreateModel( + name="IDCounter", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("count", models.IntegerField(default=10000)), + ], + options={ + "db_table": "IDCounter", + }, + ), + migrations.CreateModel( + name="MembershipType", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.CharField(max_length=50, unique=True, verbose_name="卡类型"), + ), + ("title", models.CharField(max_length=100, verbose_name="标题")), + ("description", models.TextField(verbose_name="描述")), + ( + "price", + models.DecimalField( + decimal_places=2, max_digits=10, verbose_name="价格" + ), + ), + ("duration_days", models.IntegerField(verbose_name="有效天数")), + ], + options={ + "verbose_name": "会员卡类型", + "verbose_name_plural": "会员卡类型", + "db_table": "membership_type", + }, + ), + migrations.CreateModel( + name="RedemptionCard", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField(max_length=100, unique=True, verbose_name="卡密"), + ), + ( + "card_type", + models.CharField( + choices=[("member", "会员卡"), ("quota", "额度卡")], + max_length=50, + verbose_name="卡类型", + ), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, verbose_name="生成日期"), + ), + ("expiry_date", models.DateTimeField(verbose_name="到期时间")), + ("is_used", models.BooleanField(default=False, verbose_name="是否已使用")), + ( + "used_by_openid", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="使用者OpenID" + ), + ), + ( + "used_by_nickname", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="使用者昵称" + ), + ), + ( + "card_creation_time", + models.DateTimeField(auto_now_add=True, verbose_name="卡密创建时间"), + ), + ( + "validity_period", + models.PositiveIntegerField(default=0, verbose_name="有效期"), + ), + ], + options={ + "verbose_name": "兑换卡", + "verbose_name_plural": "兑换卡", + "db_table": "redemption_card", + }, + ), + migrations.CreateModel( + name="TransactionLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "transaction_no", + models.CharField(max_length=255, unique=True, verbose_name="交易编号"), + ), + ( + "transaction_status", + models.CharField( + choices=[ + ("pending", "待处理"), + ("completed", "完成"), + ("failed", "失败"), + ], + default="pending", + max_length=50, + verbose_name="交易状态", + ), + ), + ( + "user_openid", + models.CharField(max_length=150, verbose_name="用户OpenID"), + ), + ( + "transaction_type", + models.CharField( + choices=[("member", "会员购买"), ("quota", "续费会员")], + max_length=50, + verbose_name="交易类型", + ), + ), + ( + "transaction_amount", + models.DecimalField( + decimal_places=2, max_digits=10, verbose_name="交易金额" + ), + ), + ( + "remark", + models.TextField( + blank=True, + choices=[ + ("day", "一天会员充值"), + ("week", "一周会员充值"), + ("month", "一月会员充值"), + ("season", "三月会员充值"), + ("year", "一年会员充值"), + ], + null=True, + verbose_name="备注", + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ], + options={ + "verbose_name": "交易记录", + "verbose_name_plural": "交易记录", + "db_table": "transaction_log", + }, + ), + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("openid", models.CharField(max_length=150, unique=True)), + ("wxid", models.CharField(blank=True, max_length=150, null=True)), + ( + "wechat_number", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="微信号" + ), + ), + ("nickname", models.CharField(blank=True, max_length=150, null=True)), + ("gender", models.CharField(blank=True, max_length=10, null=True)), + ("region", models.CharField(blank=True, max_length=150, null=True)), + ("email", models.EmailField(blank=True, max_length=254, null=True)), + ("phone", models.CharField(blank=True, max_length=20, null=True)), + ("password", models.CharField(blank=True, max_length=128, null=True)), + ("scene", models.CharField(blank=True, max_length=100, null=True)), + ("is_member", models.BooleanField(default=False)), + ("member_start_time", models.BigIntegerField(blank=True, null=True)), + ("member_end_time", models.BigIntegerField(blank=True, null=True)), + ("is_active", models.BooleanField(default=True)), + ("usage_count", models.IntegerField(blank=True, default=0, null=True)), + ("coins", models.IntegerField(default=100, verbose_name="金币数量")), + ( + "balance", + models.DecimalField( + decimal_places=2, + default=0.0, + max_digits=10, + verbose_name="用户余额", + ), + ), + ( + "inviter_nickname", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="邀请人昵称" + ), + ), + ( + "invitees_count", + models.IntegerField(default=0, verbose_name="邀请人数量"), + ), + ( + "daily_video_quota", + models.IntegerField(default=5, verbose_name="每日视频生成额度"), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "db_table": "User", + }, + ), + migrations.CreateModel( + name="VideoExtractionRecord", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "extraction_link", + models.URLField(max_length=2048, verbose_name="提取链接"), + ), + ( + "source", + models.CharField( + choices=[("index", "Index"), ("weixin", "Weixin")], + max_length=100, + verbose_name="来源", + ), + ), + ( + "openid", + models.CharField( + blank=True, max_length=150, null=True, verbose_name="openid" + ), + ), + ( + "nickname", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="用户标识" + ), + ), + ( + "video_title", + models.TextField(blank=True, null=True, verbose_name="视频标题"), + ), + ( + "wxid", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="微信id" + ), + ), + ( + "wechat_alias", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="微信号" + ), + ), + ( + "is_successful", + models.CharField( + blank=True, max_length=255, null=True, verbose_name="是否提取成功" + ), + ), + ( + "exception_reason", + models.TextField(blank=True, null=True, verbose_name="异常原因"), + ), + ( + "extraction_time", + models.DateTimeField(auto_now_add=True, verbose_name="提取时间"), + ), + ], + options={ + "verbose_name": "视频提取记录", + "verbose_name_plural": "视频提取记录", + "db_table": "VideoExtractionRecord", + }, + ), + migrations.CreateModel( + name="VideoTask", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("openid", models.CharField(max_length=150)), + ("nickname", models.CharField(max_length=150)), + ("task_id", models.CharField(max_length=255, unique=True)), + ("task_type", models.CharField(max_length=50)), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "待处理"), + ("IN_PROGRESS", "处理中"), + ("RUNNING", "运行中"), + ("SUCCEEDED", "成功"), + ("FAILED", "失败"), + ], + default="PENDING", + max_length=20, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("text_prompt", models.TextField(null=True)), + ("width", models.IntegerField(null=True)), + ("height", models.IntegerField(null=True)), + ("motion_score", models.IntegerField(null=True)), + ("style", models.CharField(blank=True, max_length=255, null=True)), + ("seconds", models.IntegerField(null=True)), + ("image_url", models.TextField(blank=True, null=True)), + ("result_url", models.TextField(blank=True, null=True)), + ("qiniu_url", models.TextField(blank=True, null=True)), + ("progress", models.FloatField(default=0.0)), + ("error_message", models.TextField(blank=True, null=True)), + ], + options={ + "db_table": "video_task", + "ordering": ["-created_at"], + }, + ), + ] diff --git a/MyApi/migrations/0002_assetlibrary.py b/MyApi/migrations/0002_assetlibrary.py new file mode 100644 index 0000000..629cd36 --- /dev/null +++ b/MyApi/migrations/0002_assetlibrary.py @@ -0,0 +1,87 @@ +# Generated by Django 5.0.3 on 2024-06-01 06:06 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="AssetLibrary", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("qiniu_url", models.URLField(verbose_name="七牛云视频URL")), + ("original_url", models.URLField(verbose_name="原始URL")), + ("duration", models.FloatField(verbose_name="时长")), + ( + "category", + models.CharField( + choices=[ + ("abandoned", "废弃"), + ("abstract_sculpture", "抽象"), + ("advertising", "广告"), + ("anime", "动漫"), + ("cine_lens", "电影镜头"), + ("cinematic", "电影"), + ("concept_art", "艺术"), + ("forestpunk", "赛博朋克"), + ("frost", "雪"), + ("graphite", "石墨"), + ("macro_photography", "宏观"), + ("pixel_art", "像素艺术"), + ("retro_photography", "复古"), + ("sci_fi_art", "科幻"), + ("thriller", "惊悚"), + ("35mm", "35mm"), + ("vector", "矢量"), + ("watercolor", "水彩"), + ], + max_length=50, + verbose_name="分类", + ), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="视频描述"), + ), + ( + "generated_at", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="生成时间" + ), + ), + ("download_count", models.IntegerField(default=0, verbose_name="下载次数")), + ( + "is_approved", + models.BooleanField(default=False, verbose_name="是否审核"), + ), + ( + "generated_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="MyApi.user", + verbose_name="生成用户", + ), + ), + ], + options={ + "verbose_name": "素材库", + "verbose_name_plural": "素材库", + "db_table": "asset_library", + "ordering": ["-generated_at"], + }, + ), + ] diff --git a/MyApi/migrations/0003_alter_assetlibrary_generated_by.py b/MyApi/migrations/0003_alter_assetlibrary_generated_by.py new file mode 100644 index 0000000..d4255a3 --- /dev/null +++ b/MyApi/migrations/0003_alter_assetlibrary_generated_by.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.3 on 2024-06-01 06:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0002_assetlibrary"), + ] + + operations = [ + migrations.AlterField( + model_name="assetlibrary", + name="generated_by", + field=models.CharField(max_length=50, null=True, verbose_name="生成用户"), + ), + ] diff --git a/MyApi/migrations/0004_alter_assetlibrary_original_url_and_more.py b/MyApi/migrations/0004_alter_assetlibrary_original_url_and_more.py new file mode 100644 index 0000000..08db132 --- /dev/null +++ b/MyApi/migrations/0004_alter_assetlibrary_original_url_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-06-01 07:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0003_alter_assetlibrary_generated_by"), + ] + + operations = [ + migrations.AlterField( + model_name="assetlibrary", + name="original_url", + field=models.TextField(verbose_name="原始URL"), + ), + migrations.AlterField( + model_name="assetlibrary", + name="qiniu_url", + field=models.TextField(verbose_name="七牛云视频URL"), + ), + ] diff --git a/MyApi/migrations/0005_transcriptiontask.py b/MyApi/migrations/0005_transcriptiontask.py new file mode 100644 index 0000000..d1e8c23 --- /dev/null +++ b/MyApi/migrations/0005_transcriptiontask.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.3 on 2024-06-01 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0004_alter_assetlibrary_original_url_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="TranscriptionTask", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uuid", models.CharField(max_length=255)), + ("video_url", models.URLField()), + ("task_id", models.CharField(max_length=255)), + ("result", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "文案提取任务", + "verbose_name_plural": "文案提取任务", + "db_table": "TranscriptionTask", + }, + ), + ] diff --git a/MyApi/migrations/0006_alter_transcriptiontask_video_url.py b/MyApi/migrations/0006_alter_transcriptiontask_video_url.py new file mode 100644 index 0000000..02975e7 --- /dev/null +++ b/MyApi/migrations/0006_alter_transcriptiontask_video_url.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.3 on 2024-06-01 08:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0005_transcriptiontask"), + ] + + operations = [ + migrations.AlterField( + model_name="transcriptiontask", + name="video_url", + field=models.TextField(), + ), + ] diff --git a/MyApi/migrations/0007_assetlibrary_description_zh_videotask_description_zh.py b/MyApi/migrations/0007_assetlibrary_description_zh_videotask_description_zh.py new file mode 100644 index 0000000..8037112 --- /dev/null +++ b/MyApi/migrations/0007_assetlibrary_description_zh_videotask_description_zh.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-06-01 10:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0006_alter_transcriptiontask_video_url"), + ] + + operations = [ + migrations.AddField( + model_name="assetlibrary", + name="description_zh", + field=models.TextField(blank=True, null=True, verbose_name="中文描述"), + ), + migrations.AddField( + model_name="videotask", + name="description_zh", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/MyApi/migrations/0008_videotask_is_processing.py b/MyApi/migrations/0008_videotask_is_processing.py new file mode 100644 index 0000000..9d6cef8 --- /dev/null +++ b/MyApi/migrations/0008_videotask_is_processing.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.3 on 2024-06-01 12:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0007_assetlibrary_description_zh_videotask_description_zh"), + ] + + operations = [ + migrations.AddField( + model_name="videotask", + name="is_processing", + field=models.BooleanField(default=False), + ), + ] diff --git a/MyApi/migrations/0009_membershiptype_daily_video_quota_and_more.py b/MyApi/migrations/0009_membershiptype_daily_video_quota_and_more.py new file mode 100644 index 0000000..b654624 --- /dev/null +++ b/MyApi/migrations/0009_membershiptype_daily_video_quota_and_more.py @@ -0,0 +1,91 @@ +# Generated by Django 5.0.3 on 2024-06-01 20:52 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0008_videotask_is_processing"), + ] + + operations = [ + migrations.AddField( + model_name="membershiptype", + name="daily_video_quota", + field=models.IntegerField(default=0, verbose_name="每日生成视频次数"), + ), + migrations.AlterField( + model_name="assetlibrary", + name="category", + field=models.CharField( + choices=[ + ("abandoned", "废弃"), + ("abstract_sculpture", "抽象"), + ("advertising", "广告"), + ("anime", "动漫"), + ("cine_lens", "电影镜头"), + ("cinematic", "电影"), + ("concept_art", "艺术"), + ("forestpunk", "赛博朋克"), + ("frost", "雪"), + ("graphite", "石墨"), + ("macro_photography", "宏观"), + ("pixel_art", "像素艺术"), + ("retro_photography", "复古"), + ("sci_fi_art", "科幻"), + ("thriller", "惊悚"), + ("35mm", "35mm"), + ("vector", "矢量"), + ("watercolor", "水彩"), + ], + max_length=50, + null=True, + verbose_name="分类", + ), + ), + migrations.AlterField( + model_name="assetlibrary", + name="download_count", + field=models.IntegerField(default=0, null=True, verbose_name="下载次数"), + ), + migrations.AlterField( + model_name="assetlibrary", + name="generated_at", + field=models.DateTimeField( + default=django.utils.timezone.now, null=True, verbose_name="生成时间" + ), + ), + migrations.AlterField( + model_name="assetlibrary", + name="is_approved", + field=models.BooleanField(default=False, null=True, verbose_name="是否审核"), + ), + migrations.AlterField( + model_name="assetlibrary", + name="original_url", + field=models.TextField(null=True, verbose_name="原始URL"), + ), + migrations.AlterField( + model_name="assetlibrary", + name="qiniu_url", + field=models.TextField(null=True, verbose_name="七牛云视频URL"), + ), + migrations.AlterField( + model_name="transactionlog", + name="remark", + field=models.TextField( + blank=True, + choices=[ + ("day", "一天会员充值"), + ("week", "一周会员充值"), + ("month", "一月会员充值"), + ("season", "三月会员充值"), + ("year", "一年会员充值"), + ("lifetime", "永久会员充值"), + ], + null=True, + verbose_name="备注", + ), + ), + ] diff --git a/MyApi/migrations/0010_user_membership_type.py b/MyApi/migrations/0010_user_membership_type.py new file mode 100644 index 0000000..0deb07d --- /dev/null +++ b/MyApi/migrations/0010_user_membership_type.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.3 on 2024-06-01 23:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0009_membershiptype_daily_video_quota_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="membership_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="MyApi.membershiptype", + verbose_name="会员卡类型", + ), + ), + ] diff --git a/MyApi/migrations/0011_remove_membershiptype_daily_video_quota_and_more.py b/MyApi/migrations/0011_remove_membershiptype_daily_video_quota_and_more.py new file mode 100644 index 0000000..4cfc744 --- /dev/null +++ b/MyApi/migrations/0011_remove_membershiptype_daily_video_quota_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.3 on 2024-06-04 01:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0010_user_membership_type"), + ] + + operations = [ + migrations.RemoveField( + model_name="membershiptype", + name="daily_video_quota", + ), + migrations.AddField( + model_name="membershiptype", + name="coins", + field=models.IntegerField(default=0, verbose_name="赠送金币数量"), + ), + migrations.AddField( + model_name="membershiptype", + name="is_quota", + field=models.BooleanField(default=False, verbose_name="是否为额度充值"), + ), + migrations.AlterField( + model_name="user", + name="coins", + field=models.IntegerField(default=3, verbose_name="金币数量"), + ), + ] diff --git a/MyApi/migrations/0012_remove_user_daily_video_quota_and_more.py b/MyApi/migrations/0012_remove_user_daily_video_quota_and_more.py new file mode 100644 index 0000000..bfcc866 --- /dev/null +++ b/MyApi/migrations/0012_remove_user_daily_video_quota_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.3 on 2024-06-04 02:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0011_remove_membershiptype_daily_video_quota_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="user", + name="daily_video_quota", + ), + migrations.RemoveField( + model_name="user", + name="membership_type", + ), + migrations.AlterField( + model_name="transactionlog", + name="remark", + field=models.TextField(blank=True, null=True, verbose_name="备注"), + ), + migrations.AlterField( + model_name="transactionlog", + name="transaction_type", + field=models.CharField(max_length=50, verbose_name="交易类型"), + ), + ] diff --git a/MyApi/migrations/0013_videotask_gif_url.py b/MyApi/migrations/0013_videotask_gif_url.py new file mode 100644 index 0000000..03da724 --- /dev/null +++ b/MyApi/migrations/0013_videotask_gif_url.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.3 on 2024-06-04 05:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0012_remove_user_daily_video_quota_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="videotask", + name="gif_url", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/MyApi/migrations/0014_remove_assetlibrary_qiniu_url_and_more.py b/MyApi/migrations/0014_remove_assetlibrary_qiniu_url_and_more.py new file mode 100644 index 0000000..a624311 --- /dev/null +++ b/MyApi/migrations/0014_remove_assetlibrary_qiniu_url_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.3 on 2024-06-04 06:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0013_videotask_gif_url"), + ] + + operations = [ + migrations.RemoveField( + model_name="assetlibrary", + name="qiniu_url", + ), + migrations.RemoveField( + model_name="videotask", + name="qiniu_url", + ), + migrations.AddField( + model_name="assetlibrary", + name="gif_url", + field=models.TextField(blank=True, null=True, verbose_name="GIF图片URL"), + ), + ] diff --git a/MyApi/migrations/0015_assetlibrary_qiniu_url_videotask_qiniu_url.py b/MyApi/migrations/0015_assetlibrary_qiniu_url_videotask_qiniu_url.py new file mode 100644 index 0000000..d9322ea --- /dev/null +++ b/MyApi/migrations/0015_assetlibrary_qiniu_url_videotask_qiniu_url.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-06-04 06:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0014_remove_assetlibrary_qiniu_url_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="assetlibrary", + name="qiniu_url", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="videotask", + name="qiniu_url", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/MyApi/migrations/0016_alter_videotask_status.py b/MyApi/migrations/0016_alter_videotask_status.py new file mode 100644 index 0000000..fce402e --- /dev/null +++ b/MyApi/migrations/0016_alter_videotask_status.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.3 on 2024-06-04 11:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("MyApi", "0015_assetlibrary_qiniu_url_videotask_qiniu_url"), + ] + + operations = [ + migrations.AlterField( + model_name="videotask", + name="status", + field=models.CharField( + choices=[ + ("SUBMITTED", "已提交"), + ("IN_PROGRESS", "处理中"), + ("SUCCESS", "成功"), + ("FAILURE", "失败"), + ], + default="PENDING", + max_length=20, + ), + ), + ] diff --git a/MyApi/migrations/__init__.py b/MyApi/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MyApi/models.py b/MyApi/models.py new file mode 100644 index 0000000..47a1680 --- /dev/null +++ b/MyApi/models.py @@ -0,0 +1,351 @@ +from django.db import models + +# Create your models here. +from datetime import datetime +from django.db import models +from django.contrib.auth.hashers import make_password,is_password_usable +from django.utils import timezone + +# 管理员模型 +class Administrator(models.Model): + username = models.CharField(max_length=150, unique=True) # 用户名,确保唯一性 + password = models.CharField(max_length=128) # 密码,将会加密存储 + role = models.CharField(max_length=100) # 角色 + is_active = models.BooleanField(default=True) # 状态,启用/禁用,默认启用 + last_login = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) # 创建时间,自动设置为当前时间 + # 保存模型之前对密码进行加密 + def save(self, *args, **kwargs): + if not is_password_usable(self.password): + self.password = make_password(self.password) + super().save(*args, **kwargs) + class Meta: + db_table = "Administrator" + +from django.db import models + +class MembershipType(models.Model): + type = models.CharField(max_length=50, unique=True, verbose_name="卡类型") # 卡类型,如天卡、周卡等 coins 额度卡 + title = models.CharField(max_length=100, verbose_name="标题") # 卡的标题 + description = models.TextField(verbose_name="描述") # 卡的描述 + price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格") # 卡的价格 + duration_days = models.IntegerField(verbose_name="有效天数") # 卡的有效期,以天为单位 + coins = models.IntegerField(default=0, verbose_name="赠送金币数量") # 赠送金币的数量 + is_quota = models.BooleanField(default=False, verbose_name="是否为额度充值") # 是否为额度充值 + + class Meta: + db_table = "membership_type" # 设置数据库表名 + verbose_name = "会员卡类型" + verbose_name_plural = "会员卡类型" + + def __str__(self): + return self.title + + +# 用户模型 +class User(models.Model): + openid = models.CharField(max_length=150, unique=True) # 用户唯一标识 + wxid = models.CharField(max_length=150, blank=True, null=True) # 微信ID + wechat_number = models.CharField(max_length=150, blank=True, null=True, verbose_name='微信号') # 微信号 + nickname = models.CharField(max_length=150, blank=True, null=True) # 用户标识 + gender = models.CharField(max_length=10, blank=True, null=True) # 性别 + region = models.CharField(max_length=150, blank=True, null=True) # 地区 + email = models.EmailField(blank=True, null=True) # 邮箱 + phone = models.CharField(max_length=20, blank=True, null=True) # 号码 + password = models.CharField(max_length=128, blank=True, null=True) # 密码 + scene = models.CharField(max_length=100, blank=True, null=True) # 用户来源 + is_member = models.BooleanField(default=False) # 是否为会员 + member_start_time = models.BigIntegerField(blank=True, null=True) # 存储会员开始时间的时间戳 + member_end_time = models.BigIntegerField(blank=True, null=True) # 存储会员到期时间的时间戳 + is_active = models.BooleanField(default=True) # 状态,启用/禁用,默认启用 + usage_count = models.IntegerField(default=0, blank=True, null=True) # 用户使用次数 + coins = models.IntegerField(default=3, verbose_name='金币数量') # 用户金币数量 + balance = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='用户余额') # 用户余额 + inviter_nickname = models.CharField(max_length=150, blank=True, null=True, verbose_name='邀请人昵称') # 用户邀请人昵称 + invitees_count = models.IntegerField(default=0, verbose_name='邀请人数量') # 邀请人数量 + created_at = models.DateTimeField(auto_now_add=True) # 用户创建时间 + class Meta: + db_table = "User" + +#添加好友表 +class FriendRequest(models.Model): + fromusername = models.CharField(max_length=255, verbose_name='微信ID') # 发起请求的微信ID + alias = models.CharField(max_length=255, blank=True, null=True, verbose_name='微信号') # 微信号 + fromnickname = models.CharField(max_length=255, blank=True, null=True, verbose_name='昵称') # 昵称 + country = models.CharField(max_length=255, blank=True, null=True, verbose_name='国家') # 国家 + province = models.CharField(max_length=255, blank=True, null=True, verbose_name='省份') # 省份 + city = models.CharField(max_length=255, blank=True, null=True, verbose_name='城市') # 城市 + sex = models.CharField(max_length=1, blank=True, null=True, verbose_name='性别') # 性别 + scene = models.CharField(max_length=255, blank=True, null=True, verbose_name='来源') # 来源 + time = models.DateTimeField(verbose_name='申请时间') # 申请时间 + towxid = models.CharField(max_length=255, verbose_name='目标微信ID') # 接收好友请求的微信ID + + class Meta: + db_table = "FriendRequest" + verbose_name = '添加好友请求' + verbose_name_plural = '添加好友请求' + + def __str__(self): + return self.fromnickname or self.fromusername + +#文案库 +from django.db import models + +class Copywriting(models.Model): + openid = models.CharField(max_length=150, blank=True, null=True, verbose_name="openid") + nickname = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户标识") + text_content = models.TextField(verbose_name="文本内容") + source = models.CharField(max_length=255, verbose_name="来源") + tag = models.CharField(max_length=255, default=None, verbose_name="标签") + popularity = models.IntegerField(default=0, verbose_name="热度") + added_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间") + is_approved = models.BooleanField(default=False, verbose_name="审核通过") # 默认未审核 + + class Meta: + db_table = "Copywriting" + verbose_name = "文案库" + verbose_name_plural = "文案库" + + def __str__(self): + return self.text_content[:50] # 返回文案的前50个字符作为字符串表示 + + +#视频提取记录 +class VideoExtractionRecord(models.Model): + extraction_link = models.URLField(verbose_name="提取链接", max_length=2048) + source = models.CharField(max_length=100, choices=[('index', 'Index'), ('weixin', 'Weixin')], verbose_name="来源") + openid = models.CharField(max_length=150, blank=True, null=True, verbose_name="openid") + nickname = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户标识") + video_title = models.TextField(blank=True, null=True, verbose_name="视频标题") + wxid = models.CharField(max_length=255, blank=True, null=True, verbose_name="微信id") + wechat_alias = models.CharField(max_length=255, blank=True, null=True, verbose_name="微信号") + is_successful = models.CharField(max_length=255,blank=True, null=True, verbose_name="是否提取成功") + exception_reason = models.TextField(blank=True, null=True, verbose_name="异常原因") + extraction_time = models.DateTimeField(auto_now_add=True, verbose_name="提取时间") + + class Meta: + db_table = "VideoExtractionRecord" + verbose_name = "视频提取记录" + verbose_name_plural = "视频提取记录" + + def __str__(self): + return f"{self.user}'s extraction record" + +#文案提取任务 +class TranscriptionTask(models.Model): + uuid = models.CharField(max_length=255) + video_url = models.TextField() + task_id = models.CharField(max_length=255) + result = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + class Meta: + db_table = "TranscriptionTask" + verbose_name = "文案提取任务" + verbose_name_plural = "文案提取任务" + def __str__(self): + return self.task_id + +#api调用日志 +class ApiCallLog(models.Model): + # 用户来源,如'index', 'weixin'等 + source = models.CharField(max_length=100, blank=True, null=True, choices=[('index', 'Index'), ('weixin', 'Weixin')], verbose_name="来源") + + # 用户的OpenID + openid = models.CharField(max_length=150, blank=True, null=True, verbose_name="openid") + + # 用户昵称 + nickname = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户标识") + + # 微信ID + wxid = models.CharField(max_length=255, blank=True, null=True, verbose_name="微信id") + + # 微信号 + wechat_alias = models.CharField(max_length=255, blank=True, null=True, verbose_name="微信号") + + # 调用的API接口名称或路径 + api_name = models.CharField(max_length=255, verbose_name="调用接口") + + # 调用结果,使用BooleanField表示成功或失败 + is_successful = models.BooleanField(default=False, verbose_name="是否成功") + + # 备注信息,可以用来记录失败原因或其他注释信息 + remarks = models.TextField(blank=True, null=True, verbose_name="备注") + + # 调用时间,使用自动设置为当前时间的DateTimeField + call_time = models.DateTimeField(auto_now_add=True, verbose_name="调用时间") + + class Meta: + db_table = "api_call_log" + verbose_name = "API调用日志" + verbose_name_plural = "API调用日志" + + def __str__(self): + return f"{self.nickname} - {self.api_name} at {self.call_time}" + +#卡密记录 +class RedemptionCard(models.Model): + code = models.CharField(max_length=100, unique=True, verbose_name="卡密") # 卡密 + card_type = models.CharField(max_length=50, choices=(('member', '会员卡'), ('quota', '额度卡')), verbose_name="卡类型") # 卡类型 + created_date = models.DateTimeField(auto_now_add=True, verbose_name="生成日期") # 卡密生成日期 + expiry_date = models.DateTimeField(verbose_name="到期时间") # 卡密到期时间 + is_used = models.BooleanField(default=False, verbose_name="是否已使用") # 是否已被使用 + used_by_openid = models.CharField(max_length=150, blank=True, null=True, verbose_name="使用者OpenID") # 使用者的OpenID + used_by_nickname = models.CharField(max_length=150, blank=True, null=True, verbose_name="使用者昵称") # 使用者的昵称 + card_creation_time = models.DateTimeField(auto_now_add=True, verbose_name="卡密创建时间") # 卡密创建时间 + validity_period = models.PositiveIntegerField(default=0, verbose_name="有效期") # 有效期天数或额度数量 + + class Meta: + db_table = "redemption_card" # 设置数据库表名 + verbose_name = "兑换卡" + verbose_name_plural = "兑换卡" + + +# 充值列表 + + +# 订单记录 +class TransactionLog(models.Model): + TRANSACTION_STATUS_CHOICES = [ + ('pending', '待处理'), + ('completed', '完成'), + ('failed', '失败') + ] + + TRANSACTION_TYPE_CHOICES = [ + ('member', '会员购买'), + ('quota', '续费会员') + ] + + + transaction_no = models.CharField(max_length=255, unique=True, verbose_name="交易编号") + transaction_status = models.CharField(max_length=50, default='pending', choices=TRANSACTION_STATUS_CHOICES, verbose_name="交易状态") + user_openid = models.CharField(max_length=150, verbose_name="用户OpenID") + transaction_type = models.CharField(max_length=50, verbose_name="交易类型") + transaction_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="交易金额") + remark = models.TextField(blank=True, null=True, verbose_name="备注") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + class Meta: + db_table = "transaction_log" + verbose_name = "交易记录" + verbose_name_plural = "交易记录" + + def __str__(self): + return f"{self.transaction_no} - {self.transaction_status}" + + +class IDCounter(models.Model): + count = models.IntegerField(default=10000) # 初始化为0或其他你想开始的数字 + + @classmethod + def get_next_id(cls): + # 这个方法获取下一个ID,并且自增存储的计数 + counter, _ = cls.objects.get_or_create(pk=1) # 假设我们只用一个计数器,其ID为1 + counter.count += 1 + counter.save() + return counter.count + + class Meta: + db_table = "IDCounter" + +class ChatRecord(models.Model): + openid = models.CharField(max_length=150,default=None) # 用户唯一标识 + nickname = models.CharField(max_length=150, verbose_name='用户昵称') # 用户昵称 + conversation_id = models.CharField(max_length=100, verbose_name='会话ID') # 会话ID + role = models.CharField(max_length=100, verbose_name='角色') # 角色 + message_content = models.TextField(verbose_name='消息内容') # 消息内容 + is_response = models.BooleanField(default=False, verbose_name='是否为AI回复') # 是否为AI回复 + timestamp = models.DateTimeField(default=timezone.now, verbose_name='时间戳') # 时间戳 + + class Meta: + db_table = "chat_record" # 数据库表名 + ordering = ['-timestamp'] # 默认排序 + verbose_name = '聊天记录' + verbose_name_plural = '聊天记录' + + def __str__(self): + return f"{self.nickname}: {self.message_content[:20]}" + +class VideoTask(models.Model): + TASK_STATUS_CHOICES = [ + ('pending','待处理'), + ('failed', '已提交'), + ('running','运行中'), + ('success', '成功'), + ('failed', '失败'), + ] + + openid = models.CharField(max_length=150) # 用户唯一标识 + nickname = models.CharField(max_length=150) # 用户昵称 + task_id = models.CharField(max_length=255, unique=True) # 任务ID + task_type = models.CharField(max_length=50) # 任务类型,例如 'gen2' + status = models.CharField(max_length=20, choices=TASK_STATUS_CHOICES, default='pending') # 任务状态 + created_at = models.DateTimeField(auto_now_add=True) # 任务创建时间 + updated_at = models.DateTimeField(auto_now=True) # 任务更新时间 + text_prompt = models.TextField(null=True) # 文本提示 + width = models.IntegerField(null=True) # 视频宽度 + height = models.IntegerField(null=True) # 视频高度 + motion_score = models.IntegerField(null=True) # 动态评分 + style = models.CharField(max_length=255, blank=True, null=True) # 风格 + seconds = models.IntegerField(null=True) # 视频时长 + image_url = models.TextField(blank=True, null=True) # 图生视频的图像URL + result_url = models.TextField(blank=True, null=True) # 视频生成结果URL + qiniu_url = models.TextField(blank=True, null=True) # 七牛云储存url + gif_url = models.TextField(blank=True, null=True) # GIF图片链接 + progress = models.FloatField(default=0.0) # 进度百分比 + error_message = models.TextField(blank=True, null=True) # 错误信息 + description_zh = models.TextField(blank=True, null=True) # 中文描述 + is_processing = models.BooleanField(default=False) # 正在处理标志 + + class Meta: + db_table = "video_task" + ordering = ['-created_at'] + + def __str__(self): + return self.task_id + + +class AssetLibrary(models.Model): + STYLE_CHOICES = [ + ('abandoned', '废弃'), + ('abstract_sculpture', '抽象'), + ('advertising', '广告'), + ('anime', '动漫'), + ('cine_lens', '电影镜头'), + ('cinematic', '电影'), + ('concept_art', '艺术'), + ('forestpunk', '赛博朋克'), + ('frost', '雪'), + ('graphite', '石墨'), + ('macro_photography', '宏观'), + ('pixel_art', '像素艺术'), + ('retro_photography', '复古'), + ('sci_fi_art', '科幻'), + ('thriller', '惊悚'), + ('35mm', '35mm'), + ('vector', '矢量'), + ('watercolor', '水彩'), + ] + + original_url = models.TextField(verbose_name='原始URL', null=True) # 原始URL + qiniu_url = models.TextField(blank=True, null=True) # 七牛云储存url + duration = models.FloatField(verbose_name='时长') # 视频时长,以秒为单位 + category = models.CharField(max_length=50, choices=STYLE_CHOICES, verbose_name='分类', null=True) # 视频分类 + description = models.TextField(verbose_name='视频描述', blank=True, null=True) # 视频描述 + description_zh = models.TextField(verbose_name='中文描述', blank=True, null=True) # 中文描述 + generated_by = models.CharField(null=True, verbose_name='生成用户', max_length=50) # 生成用户 + generated_at = models.DateTimeField(default=timezone.now, verbose_name='生成时间', null=True) # 生成时间 + download_count = models.IntegerField(default=0, verbose_name='下载次数', null=True) # 下载次数 + is_approved = models.BooleanField(default=False, verbose_name='是否审核', null=True) # 是否审核 + gif_url = models.TextField(verbose_name='GIF图片URL', blank=True, null=True) # GIF图片URL + + class Meta: + db_table = "asset_library" # 数据库表名 + verbose_name = '素材库' + verbose_name_plural = '素材库' + ordering = ['-generated_at'] # 默认排序 + + def __str__(self): + return self.original_url diff --git a/MyApi/sparkAPI.py b/MyApi/sparkAPI.py new file mode 100644 index 0000000..9e8900d --- /dev/null +++ b/MyApi/sparkAPI.py @@ -0,0 +1,170 @@ +# coding: utf-8 +import _thread as thread +import os +import time +import base64 + +import base64 +import datetime +import hashlib +import hmac +import json +from urllib.parse import urlparse +import ssl +from datetime import datetime +from time import mktime +from urllib.parse import urlencode +from wsgiref.handlers import format_date_time + +import websocket + +class Ws_Param(object): + # 初始化 + def __init__(self, APPID, APIKey, APISecret, gpt_url): + self.APPID = APPID + self.APIKey = APIKey + self.APISecret = APISecret + self.host = urlparse(gpt_url).netloc + self.path = urlparse(gpt_url).path + self.gpt_url = gpt_url + + # 生成url + def create_url(self): + # 生成RFC1123格式的时间戳 + now = datetime.now() + date = format_date_time(mktime(now.timetuple())) + + # 拼接字符串 + signature_origin = "host: " + self.host + "\n" + signature_origin += "date: " + date + "\n" + signature_origin += "GET " + self.path + " HTTP/1.1" + + # 进行hmac-sha256进行加密 + signature_sha = hmac.new(self.APISecret.encode('utf-8'), signature_origin.encode('utf-8'), + digestmod=hashlib.sha256).digest() + + signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8') + + authorization_origin = f'api_key="{self.APIKey}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_sha_base64}"' + + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + + # 将请求的鉴权参数组合为字典 + v = { + "authorization": authorization, + "date": date, + "host": self.host + } + # 拼接鉴权参数,生成url + url = self.gpt_url + '?' + urlencode(v) + # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致 + return url + +# import openpyxl +# from concurrent.futures import ThreadPoolExecutor, as_completed +# import os + + +class SparkUtil(object): + # 初始化 + def __init__(self): + self.isClose = False + self.data = '' + + appid = "cb0bb4de" + api_secret = "MDI5MGNhOThlNTQxNTBhY2ZjODNkM2Fh" + api_key = "dfd17a540cf52db34c98aac216a0473c" + gpt_url = "wss://spark-api.xf-yun.com/v3.5/chat" + domain = "generalv3.5" + + wsParam = Ws_Param(appid, api_key, api_secret, gpt_url) + websocket.enableTrace(False) + wsUrl = wsParam.create_url() + + self.ws = websocket.WebSocketApp(wsUrl, on_message=self.on_message, on_error=self.on_error, + on_close=self.on_close, + on_open=self.on_open) + self.ws.appid = appid + self.ws.domain = domain + + # 收到websocket错误的处理 + def on_error(self, ws, error): + self.isClose = True + print("###--- error:", error) + + # 收到websocket关闭的处理 + def on_close(self, ws,close_status_code, close_msg): + self.isClose = True + print("### closed ###------") + + # 获取数据 + def query_and_get_data(self, query): + self.ws.query = query + self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) + while True: + if self.isClose: + self.isClose = False + result = self.data + self.data = '' + return result + + # 收到websocket连接建立的处理 + def on_open(self, ws): + thread.start_new_thread(self.run, (ws,)) + + def run(self, ws, *args): + data = json.dumps(self.gen_params(appid=ws.appid, query=ws.query, domain=ws.domain)) + ws.send(data) + + # 收到websocket消息的处理 + def on_message(self, ws, message): + # print(message) + data = json.loads(message) + code = data['header']['code'] + if code != 0: + print(f'请求错误: {code}, {data}') + self.isClose = True + ws.close() + else: + choices = data["payload"]["choices"] + status = choices["status"] + content = choices["text"][0]["content"] + self.data += content + print(content, end='') + if status == 2: + print("#### 关闭会话") + self.isClose = True + ws.close() + + + def gen_params(self, appid, query, domain): + """ + 通过appid和用户的提问来生成请参数 + """ + + data = { + "header": { + "app_id": appid, + "uid": "文案助手", + # "patch_id": [] #接入微调模型,对应服务发布后的resourceid + }, + "parameter": { + "chat": { + "domain": domain, + "temperature": 0.5, + "max_tokens": 4096, + "auditing": "default", + } + }, + "payload": { + "message": { + "text": [{"role": "user", "content": query}] + } + } + } + return data + + +if __name__ == "__main__": + data = SparkUtil().query_and_get_data("帮我写一篇一百字的作文") + print(data) diff --git a/MyApi/tests.py b/MyApi/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/MyApi/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/MyApi/urls.py b/MyApi/urls.py new file mode 100644 index 0000000..31fad11 --- /dev/null +++ b/MyApi/urls.py @@ -0,0 +1,40 @@ +# myapp/urls.py +from django.urls import path +from . import views +from . import video_text +from . import wxpay +from . import AiMssage +urlpatterns = [ + path('login/', views.miniapp_login, name='miniapp_login'), + path('video/', views.video_extraction, name='video_extraction'), + path('add_friend/', views.add_friend_request, name='add_friend_request'), + path('total/', views.total, name='total'), + # path('record_extraction', views.record_extraction, name='record_extraction'), + # path('video_transcribe/', views.video_transcribe, name='video_transcribe'), + path('rewrite_text/', views.rewrite_text, name='rewrite_text'), + path('get_copywriting_list_with_filters/', views.get_copywriting_list_with_filters,name='get_copywriting_list_with_filters'), + # path('create_copywriting/', views.create_copywriting_entry, name='create_copywriting'), + path('userinfo/', views.userinfo, name='userinfo'), + path('redeem_card/', views.redeem_card, name='redeem_card'), + path('reward_invitation', views.reward_invitation, name='reward_invitation'), + path('video_to_text/', video_text.transcribe_audio, name='video_to_text'), + path('query_task/', video_text.query_task_status, name='query_task'), + path('wx_pay/', wxpay.wx_pay, name='wx_pay'), + path('wx_pay_notify/', wxpay.wx_pay_notify, name='wx_pay_notify'), + path('upload_image/', views.UploadImageView.as_view(), name='upload_image'), + path('generate_video/', views.GenerateVideoView.as_view(), name='generate_video'), + path('task_status//', views.TaskStatusView.as_view(), name='task_status'), + path('extend_video/', views.ExtendVideoView.as_view(), name='extend_video'), + path('generate_image_video/', views.GenerateImageVideoView.as_view(), name='generate_image_video'), + path('user//video_tasks/', views.UserVideoTaskListView.as_view(), name='user_video_tasks'), + path('user//delete_task//', views.DeleteVideoTaskView.as_view(), name='delete_task'), + path('call_bailian_app/', AiMssage.call_bailian_app, name='call_bailian_app'), + path('get_recent_chat_records/', AiMssage.get_recent_chat_records, name='get_recent_chat_records'), + path('asset_library/', views.AssetLibraryView.as_view(), name='asset_library'), + path('update_pending_tasks/',views.UpdatePendingTasksView.as_view(), name='update_pending_tasks'), + path('increment-download-count/', views.increment_download_count, name='increment_download_count'), + path('reset-daily-quota/', views.DailyCoinsBonusView.as_view(), name='reset_daily_quota'), + path('video-generation-callback/', views.VideoGenerationCallbackView.as_view(), name='VideoGenerationCallbackView'), + +] + diff --git a/MyApi/video_text.py b/MyApi/video_text.py new file mode 100644 index 0000000..50ac66a --- /dev/null +++ b/MyApi/video_text.py @@ -0,0 +1,177 @@ +import base64 +import json +import logging +import os +from tencentcloud.common import credential +from tencentcloud.common.profile.client_profile import ClientProfile +from tencentcloud.common.profile.http_profile import HttpProfile + +from tencentcloud.asr.v20190614 import asr_client, models +from django.http import JsonResponse +from Crypto.Cipher import AES +import base64 + +from .API_Log import get_logger +from .models import TranscriptionTask + +from .views import update_usage_count +# 使用示例 +log_file = '文案提取日志.log' +logger = get_logger('文案提取日志', log_file, when='midnight', backup_count=7) + +def decrypt_param(x, t): + # 创建AES解密器对象 + key = b'qw5w6SFE2D1jmxyd' + iv = b'345GDFED433223DF' + + # 检查 x 和 t 是否为空 + if not x or not t: + return False + + try: + cipher = AES.new(key, AES.MODE_CBC, iv) + # 将密文进行Base64解码 + ciphertext_bytes = base64.b64decode(x) + # 使用解密器解密密文 + plaintext_bytes = cipher.decrypt(ciphertext_bytes) + # 删除填充字符 + plaintext = plaintext_bytes.rstrip(b'\0').decode('utf-8') + # 比较解密后的明文和 t + if plaintext.rstrip('\x03') == t.rstrip('\x03'): + return True + else: + return False + except Exception as e: + print(f"解密过程出现错误: {e}") + return False + + +# 数据库操作函数 +def get_task_from_db(video_url): + try: + return TranscriptionTask.objects.get(video_url=video_url) + except TranscriptionTask.DoesNotExist: + return None + + +def save_task_to_db(uuid, video_url, task_id, result=None): + task = TranscriptionTask(uuid=uuid, video_url=video_url, task_id=task_id, result=result) + task.save() + + +def get_task_result_from_db(task_id): + try: + task = TranscriptionTask.objects.get(task_id=task_id) + return task.result + except TranscriptionTask.DoesNotExist: + return None + + +def update_task_result_in_db(task_id, result): + try: + task = TranscriptionTask.objects.get(task_id=task_id) + task.result = result + task.save() + except TranscriptionTask.DoesNotExist: + pass + + +def transcribe_audio(request): + data = json.loads(request.body) + video_url = data.get('url') + uuid = data.get('uuid') + openid = data.get('openid') + x = request.headers.get('X', '') + t = request.headers.get('T', '') + + if not uuid or not video_url: + logger.error( + f'API 文案提取: 【缺少必要参数】\n headers={request.headers}\n data={data} \n ---------------------------------') + return JsonResponse({'error': '缺少必要参数'}) + + if decrypt_param(x, t): + existing_task = get_task_from_db(video_url) + if existing_task: + if existing_task.result: + return JsonResponse({'result': existing_task.result}) + else: + return JsonResponse({'task_id': existing_task.task_id}) + else: + increment = -1 # 假设每次生成视频需要消耗一次使用次数 + function_type = 'transcribe_audio' + result = update_usage_count(openid, increment, function_type) + if not result['success']: + return JsonResponse(result) + + cred = credential.Credential("AKIDb6YsAMQL4hBjVGybN6gzLeu7FUegcins", "MpYQQbp8UxQ2pbIKXrCX5d2hjx9Th2Or") + http_profile = HttpProfile() + http_profile.endpoint = "asr.tencentcloudapi.com" + client_profile = ClientProfile() + client_profile.httpProfile = http_profile + client = asr_client.AsrClient(cred, "ap-guangzhou", client_profile) + req = models.CreateRecTaskRequest() + req.EngineModelType = "16k_zh" + req.ChannelNum = 1 + req.ResTextFormat = 0 + req.SourceType = 0 + req.Url = video_url + resp = client.CreateRecTask(req) + resp_dict = json.loads(resp.to_json_string()) + + task_id = resp_dict['Data']['TaskId'] + save_task_to_db(uuid, video_url, task_id) + + logger.info( + f'API 文案提取: 【{uuid}--{video_url} 提交任务成功】\n 返回结果={resp}\n---------------------------------') + return JsonResponse(resp_dict, safe=False) + else: + logger.error( + f'API 文案提取: 【非法参数】\n headers={request.headers}\n data={data} \n ---------------------------------') + return JsonResponse({'error': '非法参数'}) + +def query_task_status(request): + data = json.loads(request.body) + logger.info( + f'API 文案提取任务查询: {data} \n ---------------------------------') + task_id = data.get('task_id') + uuid = data.get('uuid') + x = request.headers.get('X', '') + t = request.headers.get('T', '') + + if not uuid or not task_id: + logger.error( + f'API 文案提取任务查询: 【缺少必要参数】\n headers={request.headers}\n data={data} \n ---------------------------------') + return JsonResponse({'error': '缺少必要参数'}) + + if decrypt_param(x, t): + cached_result = get_task_result_from_db(task_id) + if cached_result: + return JsonResponse({'result': cached_result}) + + cred = credential.Credential("AKIDb6YsAMQL4hBjVGybN6gzLeu7FUegcins", "MpYQQbp8UxQ2pbIKXrCX5d2hjx9Th2Or") + http_profile = HttpProfile() + http_profile.endpoint = "asr.tencentcloudapi.com" + clientProfile = ClientProfile() + clientProfile.httpProfile = http_profile + client = asr_client.AsrClient(cred, "ap-guangzhou", clientProfile) + req = models.DescribeTaskStatusRequest() + params = { + "TaskId": int(task_id) + } + req.from_json_string(json.dumps(params)) + + resp = client.DescribeTaskStatus(req) + resp_dict = json.loads(resp.to_json_string()) + + if resp_dict['Data']['Status'] == 2: + result = resp_dict['Data']['Result'] + update_task_result_in_db(task_id, result) + return JsonResponse({'result': result}) + + logger.info( + f'API 文案提取任务查询: 【{uuid}--{task_id} 进度查询】\n 返回结果={resp}\n---------------------------------') + return JsonResponse(resp_dict, safe=False) + else: + logger.error( + f'API 文案提取任务查询: 【非法参数】\n headers={request.headers}\n data={data} \n ---------------------------------') + return JsonResponse({'error': '非法参数'}) \ No newline at end of file diff --git a/MyApi/views.py b/MyApi/views.py new file mode 100644 index 0000000..457aa99 --- /dev/null +++ b/MyApi/views.py @@ -0,0 +1,1260 @@ +import os +import tempfile +import time +import re +import http.client +import hashlib +import urllib +import random + +from django.core.files.storage import FileSystemStorage +from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +from django.db.models import Q +from django.forms import model_to_dict +from django.http import JsonResponse +import requests +import json +from datetime import datetime, timedelta +import jwt # 用于生成Token +from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views import View +import qiniu +from django.views.decorators.csrf import csrf_exempt + +from website import settings +from .WXBizDataCrypt import WXBizDataCrypt # 引入微信解密库 +from django.contrib.auth.hashers import make_password +from .models import Administrator, FriendRequest, User, VideoExtractionRecord, ApiCallLog, Copywriting, MembershipType, \ + RedemptionCard, IDCounter, VideoTask, AssetLibrary +from urllib.parse import quote +from Crypto.Cipher import AES +import base64 +from django.utils.dateparse import parse_datetime +# 你的Django设置中的小程序配置 +from MyApi.sparkAPI import SparkUtil +from .API_Log import get_logger +log_file = '小程序日志.log' +logger = get_logger('小程序日志', log_file, when='midnight', backup_count=7) +APPID = 'wxb5a1857369e5809e' +SECRET = 'cbdb58343de23967d59fb90c7143a6b0' +from django.utils import timezone +import shortuuid +from threading import Thread + +def is_valid_text(text): + text = urllib.parse.unquote(text) + # 定义正则表达式,匹配除了空白字符外的任意字符 + regex = r'\S' + # 使用正则表达式进行匹配 + return re.search(regex, text) is not None + +def contains_sql_or_xss(input_string): + """ + 检测输入字符串是否包含SQL注入或XSS攻击的内容 + """ + input_string = urllib.parse.unquote(input_string) + # SQL注入检测:检查是否包含SQL关键字或语句 + sql_keywords = ['SELECT', 'UPDATE', 'DELETE', 'INSERT', 'DROP', 'ALTER', 'TRUNCATE', 'EXEC', 'UNION', 'FROM', 'WHERE', 'JOIN' ,'ldap' ,'~' , '/' ,'\/'] + for keyword in sql_keywords: + if keyword.lower() in input_string.lower(): + return True + + # XSS攻击检测:检查是否包含HTML标签和JavaScript代码 + # 使用正则表达式检测包含HTML标签和JavaScript代码的情况 + pattern = re.compile(r'<[^>]+>|javascript:', re.IGNORECASE) + if pattern.search(input_string): + return True + + return False + +def decrypt_param(x, t): + # 创建AES解密器对象 + key = b'qw5w6SFE2D1jmxyd' + iv = b'345GDFED433223DF' + + # 检查 x 和 t 是否为空 + if not x or not t: + return False + + try: + cipher = AES.new(key, AES.MODE_CBC, iv) + # 将密文进行Base64解码 + ciphertext_bytes = base64.b64decode(x) + # 使用解密器解密密文 + plaintext_bytes = cipher.decrypt(ciphertext_bytes) + # 删除填充字符 + plaintext = plaintext_bytes.rstrip(b'\0').decode('utf-8') + # 比较解密后的明文和 t + if plaintext.rstrip('\x03') == t.rstrip('\x03'): + return True + else: + return False + except Exception as e: + print(f"解密过程出现错误: {e}") + return False + +#用于判断次数剩余并增加或者减少额度 +def update_usage_count(openid, increment, function_type): + try: + user = User.objects.get(openid=openid) + + # 判断会员是否过期,如果过期则更新会员字段为False + if user.member_end_time is not None and user.member_end_time < timezone.now().timestamp(): + user.is_member = False + user.membership_type = None + user.save() + logger.info(f'会员 {user.id} (openid: {openid}) 已过期,已更新为非会员') + + # 定义需要扣除次数的功能 GenerateVideoView ExtendVideoView GenerateImageVideoView + functions_require_coins_for_members = ['GenerateVideoView','ExtendVideoView','GenerateImageVideoView'] # 替换为实际功能名 + + # 增加或减少用户使用次数 + if increment > 0: + user.coins += increment + logger.info(f'用户 {user.id} (openid: {openid}) 使用次数增加 {increment} 次') + user.save() + return {'success': True} + elif increment < 0: + if user.is_member: + if function_type in functions_require_coins_for_members: + if user.coins + increment >= 0: + user.coins += increment + logger.info(f'会员用户 {user.id} (openid: {openid}) 使用功能 {function_type} 使用次数减少 {abs(increment)} 次 剩余 {user.coins}') + user.save() + return {'success': True} + else: + logger.warning(f'会员用户 {user.id} (openid: {openid}) 使用功能 {function_type} 使用次数不足以减少 {abs(increment)} 次') + return {'success': False, 'message': '次数不足,请充值'} + else: + logger.info(f'会员用户 {user.id} (openid: {openid}) 使用功能 {function_type} 不需要扣除次数') + return {'success': True} + else: + if user.coins + increment >= 0: + user.coins += increment + logger.info(f'用户 {user.id} (openid: {openid}) 使用功能 {function_type} 使用次数减少 {abs(increment)} 次 剩余 {user.coins}') + user.save() + return {'success': True} + else: + logger.warning(f'用户 {user.id} (openid: {openid}) 使用功能 {function_type} 使用次数不足以减少 {abs(increment)} 次') + return {'success': False, 'message': '次数不足,请充值'} + return {'success': True} + except User.DoesNotExist: + logger.error(f'用户 (openid: {openid}) 不存在') + return {'success': False, 'message': '用户不存在'} + +#用于卡密兑换功能 +def update_user_membership(openid, duration_days): + current_time = int(time.time()) # 获取当前时间戳 + + try: + user = User.objects.get(openid=openid) + # 更新会员状态和时间 + user.is_member = True + + if user.member_start_time is None: # 如果用户是第一次开会员 + user.member_start_time = current_time + logger.info(f'第一次开会员:{current_time} -- {current_time + duration_days * 24 * 3600}') + elif user.member_end_time is None or user.member_end_time < current_time: # 如果会员已经过期 + logger.info(f'过期:{current_time} -- {current_time + duration_days * 24 * 3600}') + user.member_start_time = current_time + + # 计算会员到期时间 + if user.member_end_time is None or user.member_end_time < current_time: + logger.info(f'第一次开会员2:{user.member_end_time} -- {current_time + duration_days * 24 * 3600}') + user.member_end_time = current_time + duration_days * 24 * 3600 # 将天数转换为秒 + else: # 如果会员尚未过期,则在现有会员结束时间上添加续费的天数 + logger.info(f'续费:{user.member_end_time} -- {user.member_end_time + duration_days * 24 * 3600}') + user.member_end_time += duration_days * 24 * 3600 + + user.save() + return True + except User.DoesNotExist: + # 处理用户不存在的情况 + return False + +#日志记录 +def log_api_call(source, openid, nickname, wxid, wechat_alias, api_name, is_successful, remarks=""): + """ + 插入API调用日志记录的函数。 + """ + ApiCallLog.objects.create( + source=source, + openid=openid, + nickname=nickname, + wxid=wxid, + wechat_alias=wechat_alias, + api_name=api_name, + is_successful=is_successful, + remarks=remarks + ) + + # log_api_call( + # source='weixin', + # openid='用户的openid', + # nickname='用户昵称', + # wxid='用户微信ID', + # wechat_alias='用户微信号', + # api_name='调用的API名称', + # is_successful=True, + # remarks='备注信息' + # ) + +def generate_short_id(): + # 使用IDCounter模型获取下一个ID + next_id = IDCounter.get_next_id() + return next_id + + +def miniapp_login(request): + data = json.loads(request.body.decode('utf-8')) + code = data.get('code') + openid = data.get('openid','') + iv = data.get('iv') + launchOptions = data.get('Options', {}) + logger.info(f' 登录 {launchOptions}') + print(launchOptions) + if not code: + return JsonResponse({'error': '缺少code参数'}, status=400) + # 使用code获取session_key和openid + url = f'https://api.weixin.qq.com/sns/jscode2session?appid={APPID}&secret={SECRET}&js_code={code}&grant_type=authorization_code' + res = requests.get(url) + result = res.json() + logger.info(f'登录接口返回:{result}') + if 'openid' in result: + openid = result['openid'] + session_key = result['session_key'] + + # 解密encryptedData获取用户信息 + # pc = WXBizDataCrypt(APPID, session_key) + # decrypted_data = pc.decrypt(encrypted_data, iv) + + # token = generate_token_for_user(openid) + # print(decrypted_data) + if openid: + user, created = User.objects.get_or_create(openid=openid) + # 如果是新用户,则设置其它属性 defaultDailyFreeParseNum totalParseNum + logger.info(f'登录接口1 openid{openid} :created {created}') + if created: + logger.info(f'登录接口2 openid{openid} :created {created}') + uuid = generate_short_id() + user.wxid = launchOptions.get('query', {}).get('wxid', '') #微信id + user.wechat_number = launchOptions.get('query', {}).get('alias', '') #微信号 + user.nickname = uuid + user.scene = launchOptions.get('scene', '') #来源 + user.inviter_nickname = launchOptions.get('inviter_nickname', '') #邀请人 + user.save() + user = User.objects.get(openid=openid) + user_info = model_to_dict(user) + logger.info(f'新用户openid {openid} --uuid{uuid} 添加成功 {user_info}') + return JsonResponse({'token': session_key, 'userInfo': user_info,}) + else: + user = User.objects.get(openid=openid) + user_info = model_to_dict(user) + logger.info(f'老用户 {openid} 进行访问 {user_info}') + return JsonResponse({'token': session_key, 'userInfo': user_info}) + + + else: + logger.error(f'没有openid参数') + return JsonResponse({'error': '没有openid参数'}, status=500) + else: + logger.error(f'微信登录失败 data{result}') + + return JsonResponse({'error': '微信登录失败'}, status=500) + + + +#获取用户信息 +def userinfo(request): + data = json.loads(request.body) + openid = data.get('openid','') + + if contains_sql_or_xss(openid): + logger.error(f'{openid} 用户触发威胁字符检测: {data}') + return JsonResponse({'status': 'error', 'message': '你的国际行为已被记录,我们将保留追责权利'}) + + if not openid: + return JsonResponse({"status": "erroe",'body': '缺少openid参数'}) + else: + increment = -0 + function_type = 'luserinfo' + result = update_usage_count(openid, increment, function_type) + membership_types = MembershipType.objects.all().values('type', 'title', 'description', 'price', 'coins','is_quota') + user = User.objects.get(openid=openid) + if user: + user_info = model_to_dict(user) + try: + + user_info['member_start_time'] = time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(user.member_start_time)) + user_info['member_end_time'] = time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(user.member_end_time)) + except: + user_info['member_start_time'] = user.member_start_time + user_info['member_end_time'] = user.member_end_time + + membership_type_list = list(membership_types) + return JsonResponse({"status": "success", 'userInfo': user_info, 'membershipTypeList': membership_type_list}) + else: + return JsonResponse({"status": "erroe", 'userInfo': None}) + +def generate_token_for_user(openid): + # 定义密钥,你应该将其保存在配置文件或环境变量中 + secret = 'oigjfs**00--++2s' + + # 定义Token的过期时间,这里设置为24小时 + expiration = datetime.utcnow() + timedelta(days=1) + + # 创建Token + token = jwt.encode({ + 'openid': openid, + 'exp': expiration + }, secret, algorithm='HS256') + + # PyJWT版本可能会影响返回值类型,确保返回字符串 + return token.decode('utf-8') if isinstance(token, bytes) else token + + +# 你的视频提取接口、userId和secretKey +API_URL = "https://h.aaaapp.cn/single_post" +USER_ID = "22F1BE410945DD40F0569BFA197A85C0" +SECRET_KEY = "7801ffd6cceaff579812214a93b948b4" + +#提取视频 +def video_extraction(request): + # 解析POST请求体 + data = json.loads(request.body) + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + + try: + logger.info(f'视频提取data: {data} ') + video_url = data.get('url') + uuid = data.get('uuid') + openid = data.get('openid') + increment = - 1 + function_type = 'video_extraction' + result = update_usage_count(openid, increment, function_type) + if result['success']: + encoded_video_url = quote(video_url, safe='') + # 这里简化了token验证逻辑,实际应用中应根据项目需求进行相应的安全校验 + if not uuid or not video_url: + logger.error(f'{encoded_video_url}视频提取失败--未登录-uuid:{uuid}--openid:{openid}') + return JsonResponse({'error': '未登录'}) + # 调用视频提取API + response = requests.post(API_URL, data={ + 'url': video_url, + 'userId': USER_ID, + 'secretKey': SECRET_KEY + }) + + if response.status_code == 200: + logger.info(f'{encoded_video_url}视频提取成功--uuid:{uuid}--openid:{openid}') + return JsonResponse(response.json()) + else: + logger.error(f'提取链接无效{response.text}') + return JsonResponse({'error': '视频提取失败'}, status=response.status_code) + else: + print(f'使用次数更新失败: {result}') + return JsonResponse(result) + except json.JSONDecodeError: + logger.error(f'视频提取出错') + return JsonResponse({'error': '无效的请求'}) + else: + return JsonResponse({'error': '非法参数'}, status=400) + + + + + + +#储存好友请求数据 +def add_friend_request(request): + if request.method == 'POST': + data = json.loads(request.body) + # 解析申请时间字符串为datetime对象 + fromusername = data.get('fromusername') + friend_request = FriendRequest.objects.filter(fromusername=fromusername).first() + if friend_request: + print('已存在') + return JsonResponse({"status": "success", "message": "已存在数据"}, status=200) + else: + data['time'] = datetime.strptime(data['time'], '%Y-%m-%d %H:%M:%S') + friend_request = FriendRequest.objects.create(**data) + friend_request.save() + print('入库成功') + return JsonResponse({"status": "success", "message": "添加好友请求已保存"}, status=200) + else: + return JsonResponse({"status": "error", "message": "只支持POST请求"}, status=405) + +#返回使用次数 +def total(request): + data = json.loads(request.body) + uuid = data.get('uuid') + + try: + user = User.objects.get(nickname=uuid) + total_count = user.usage_count + except User.DoesNotExist: + total_count = 0 + + return JsonResponse({'total_num': total_count}) + + + +#改写文案接口 +def rewrite_text(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + logger.info(f'文案改写开始{data}') + text = data.get('text') + uuid = data.get('uuid') + openid = data.get('openid','') + type = data.get('type', '') + wxid = data.get('wxid', '') + alias = data.get('alias', '') + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + # 确保text参数存在 + if not text: + logger.error(f'缺少必要的文案参数--uuid:{uuid} --openid:{openid}') + return JsonResponse({'error': '缺少必要的文案参数'}, status=400) + query = f'{text}。【改写这篇文案,要求吸引人,引起人们共鸣,如果有错别字或者不通顺语句请改正,字数需要和原文差不多】' + # 返回改写后的文案 + #增加提取记录 + if openid == '' or openid == None: + logger.error(f'改写文案失败 未登录-uuid:{uuid}--openid:{openid}') + return JsonResponse({'error': '未登录,请重试'}) + else: + rest = SparkUtil().query_and_get_data(query) + increment = -1 + function_type = 'video_extraction' + result = update_usage_count(openid, increment, function_type) + if result['success']: + # 提取成功用户使用次数+1 + user = User.objects.get(openid=openid) + user.usage_count += 1 + user.save() + logger.info(f'改写文案成功-{rest}-uuid:{uuid}--openid:{openid}') + return JsonResponse({ + 'status': 'success', + # 'original_text': text, + 'rewritten_text': rest + }) + else: + logger.error(f'ai改写文案次数不足或不是会员-{rest}-uuid:{uuid}--openid:{openid}') + return JsonResponse(result) + else: + logger.error( + f'改写文案: 【非法参数】\n headers={request.headers}\n data={data} \n ---------------------------------') + return JsonResponse({'error': '非法参数'}) + + except json.JSONDecodeError: + logger.error(f'ai改写文案----无效的请求格式') + return JsonResponse({'error': '无效的请求格式'}) + else: + logger.error(f'ai改写文案----仅支持POST请求') + return JsonResponse({'error': '仅支持POST请求'}) + + + +def get_copywriting_list_with_filters(request): + """ + 返回带有搜索、分类和排序功能的分页文案列表,只包括已审核的文案 + """ + # 获取请求参数 + page = request.GET.get('page', 1) + page_size = request.GET.get('page_size', 10) + search_query = request.GET.get('search', '') + search_query = quote(search_query) + sort_by = request.GET.get('sort_by', 'default') # 默认按综合排序 + category = request.GET.get('category', '') + + # 构建查询条件 + query_conditions = Q(text_content__icontains=search_query) & Q(is_approved=True) # 只选择已审核的文案 + + # 如果有分类条件,则进一步过滤 + if category: + query_conditions &= Q(tag__icontains=category) + + # 根据查询条件查询文案记录 + if sort_by == 'default': + copywriting_list = Copywriting.objects.filter(query_conditions).order_by('-id', '-added_time') + elif sort_by == 'popularity': + # 按热度排序 + copywriting_list = Copywriting.objects.filter(query_conditions).order_by('-popularity', '-added_time') + elif sort_by == 'time': + # 按时间排序 + copywriting_list = Copywriting.objects.filter(query_conditions).order_by('-added_time') + else: + # 默认排序:先按热度,然后按时间 + copywriting_list = Copywriting.objects.filter(query_conditions).order_by('-id', '-added_time') + + # 使用Django的Paginator进行分页 + paginator = Paginator(copywriting_list, page_size) + + try: + copywritings = paginator.page(page) + except PageNotAnInteger: + copywritings = paginator.page(1) + except EmptyPage: + copywritings = paginator.page(paginator.num_pages) + + # 获取当前页的数据 + current_page_data = list(copywritings.object_list.values('id', 'text_content', 'tag', 'popularity')) + + # 构造响应数据 + data = { + 'status': 'success', + 'data': current_page_data, + 'current_page': copywritings.number, + 'total_pages': paginator.num_pages, + } + + return JsonResponse(data) + + + + +#卡密兑换功能 +def redeem_card(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + card_code = data.get('card_code') + openid = data.get('openid') + uuid = data.get('uuid') + # 根据卡密查询兑换卡信息 + logger.info(f'卡密兑换获取到数据: {data}') + try: + card = RedemptionCard.objects.get(code=card_code) + except RedemptionCard.DoesNotExist: + logger.error(f'无效的卡密: {card_code}') + return JsonResponse({'error': '无效的卡密'}) + + # 检查卡密是否已经被使用 + if card.is_used: + logger.error(f'该卡密已被使用: {card_code}') + return JsonResponse({'error': '该卡密已被使用'}) + + # 检查卡密是否过期 + if card.expiry_date < timezone.now(): + logger.error(f'该卡密已过期: {card_code}') + return JsonResponse({'error': '该卡密已过期'}) + + # 根据卡类型执行相应操作 + if card.card_type == 'member': + # 如果是会员卡,调用更新会员函数 + success = update_user_membership(openid, card.validity_period) + if success: + card.used_by_openid = openid + card.is_used = True + card.used_by_nickname = uuid + card.save() + logger.info(f' {openid} 成功领取: {card.validity_period}天会员 {card_code}') + return JsonResponse({"status": "success", 'message': f'成功领取{card.validity_period}天会员'}) + else: + return JsonResponse({'error': '卡密激活失败,请联系管理员'}) + elif card.card_type == 'quota': + # 如果是额度卡,调用更新使用次数函数 + success = update_usage_count(openid, increment=+card.validity_period) + if success.get('success'): + card.used_by_openid = openid + card.is_used = True + card.used_by_nickname = uuid + card.save() + logger.info(f' {openid} 成功领取:{card.validity_period}次额度 {card_code}') + return JsonResponse({"status": "success", 'message': f'成功领取{card.validity_period}次额度'}) + else: + logger.error(f' 卡密激活失败,请联系管理员 {card_code}') + return JsonResponse({'error': '卡密激活失败,请联系管理员'}) + else: + logger.error(f' 卡密激活失败 {card_code}') + return JsonResponse({'error': '卡密激活失败'}) + else: + return JsonResponse({'error': '非法参数'}, status=400) + + except Exception as e: + logger.error(f' 处理请求时出错') + return JsonResponse({'error': '处理请求时出错:' + str(e)}) + + else: + logger.error(f' 卡密兑换接口 -- 仅支持POST请求') + return JsonResponse({'error': '仅支持POST请求'}) + +#邀请用户接口 +def reward_invitation(request): + if request.method == 'POST': + try: + data = json.loads(request.body) + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + logger.info(f'邀请激励数据: {data}') + openid = data.get('openid', '') + uuid = data.get('uuid', '') + + if openid and uuid: + # 查找uuid对应的用户,确保是新用户 + try: + invited_user = User.objects.get(nickname=uuid) + logger.info(f'用户 {uuid} 已经存在,不能重复邀请') + return JsonResponse({'success': False, 'message': '用户已存在,不能重复邀请'}) + except User.DoesNotExist: + # 被邀请用户不存在,说明是新用户,可以发放奖励 + try: + inviter_user = User.objects.get(openid=openid) + except User.DoesNotExist: + logger.error(f'邀请人用户 {openid} 不存在') + return JsonResponse({'success': False, 'error': '邀请人用户不存在'}) + + # 更新邀请人信息 + inviter_user.invitees_count += 1 + inviter_user.coins += 10 + inviter_user.save() + + # 创建被邀请用户并设置邀请人信息 + new_user = User(nickname=uuid, inviter_nickname=inviter_user.nickname) + new_user.save() + + logger.info(f'用户 {openid} 邀请新用户 {uuid} 成功,奖励已发放') + return JsonResponse({'success': True, 'message': '奖励发放成功'}) + + else: + logger.error(f'用户邀请奖励接口 缺少必要参数') + return JsonResponse({'success': False, 'error': '缺少必要参数'}) + else: + return JsonResponse({'success': False, 'error': '非法参数'}) + except User.DoesNotExist: + logger.error(f'用户不存在') + return JsonResponse({'success': False, 'error': '用户不存在'}) + except Exception as e: + logger.error(f'发放奖励时出错: {str(e)}') + return JsonResponse({'success': False, 'error': '服务器内部错误'}) + else: + logger.error(f'用户邀请奖励接口 请求方法不支持') + return JsonResponse({'success': False, 'error': '请求方法不支持'}) +def upload_to_qiniu(file_path, key): + access_key = settings.QINIU_ACCESS_KEY + secret_key = settings.QINIU_SECRET_KEY + bucket_name = settings.QINIU_BUCKET_NAME + bucket_domain = settings.QINIU_DOMAIN + + q = qiniu.Auth(access_key, secret_key) + token = q.upload_token(bucket_name, key) + + ret, info = qiniu.put_file(token, key, file_path) + + if info.status_code == 200: + encoded_url = f"https://{bucket_domain}/{quote(key)}" + return encoded_url + else: + raise Exception("上传到七牛云失败") + + + +class UploadImageView(View): + @method_decorator(csrf_exempt) + def post(self, request): + image_file = request.FILES.get('image_file') + if not image_file: + return JsonResponse({"message": "没有提供图像文件"}, status=400) + + if not image_file.content_type.startswith('image/'): + return JsonResponse({"message": "文件格式不支持,只支持图片文件"}, status=400) + + try: + # 将文件保存到本地文件夹 + fs = FileSystemStorage() + filename = fs.save(image_file.name, image_file) + file_url = fs.url(filename) + + return JsonResponse({"message": "文件上传成功", "file_url": file_url}, status=200) + + except Exception as e: + return JsonResponse({"message": "内部服务器错误", "error": str(e)}, status=500) + + +appid = '20240601002067404' # 填写你的appid +secretKey = '6pRZ9HCSqGuMqzLO55hB' # 填写你的密钥 + +def translate(text, from_lang='auto', to_lang='en'): + myurl = '/api/trans/vip/translate' + salt = random.randint(32768, 65536) + sign = appid + text + str(salt) + secretKey + sign = hashlib.md5(sign.encode()).hexdigest() + myurl = (myurl + '?appid=' + appid + '&q=' + urllib.parse.quote(text) + + '&from=' + from_lang + '&to=' + to_lang + '&salt=' + str(salt) + + '&sign=' + sign) + + try: + httpClient = http.client.HTTPConnection('api.fanyi.baidu.com') + httpClient.request('GET', myurl) + + # response是HTTPResponse对象 + response = httpClient.getresponse() + result_all = response.read().decode("utf-8") + result = json.loads(result_all) + return result['trans_result'][0]['dst'] + + except Exception as e: + return str(e) + + finally: + if httpClient: + httpClient.close() + + + + + +class UserVideoTaskListView(View): + def get(self, request, user_id): + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + try: + video_tasks = VideoTask.objects.filter(nickname=user_id).order_by('-created_at') + page = request.GET.get('page', 1) # 获取页码参数,默认为第一页 + page_size = request.GET.get('page_size', 10) # 每页显示的条数,默认为10 + + paginator = Paginator(video_tasks, page_size) + try: + tasks_page = paginator.page(page) + except PageNotAnInteger: + tasks_page = paginator.page(1) + except EmptyPage: + tasks_page = paginator.page(paginator.num_pages) + + task_list = [] + for task in tasks_page: + task_list.append({ + 'openid': task.openid, + 'nickname': task.nickname, + 'task_id': task.task_id, + 'task_type': task.task_type, + 'status': task.status, + 'created_at': task.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'updated_at': task.updated_at.strftime('%Y-%m-%d %H:%M:%S'), + 'text_prompt': task.description_zh, + 'width': task.width, + 'height': task.height, + 'motion_score': task.motion_score, + 'style': task.style, + 'seconds': task.seconds, + 'image_url': task.image_url, + 'result_url': task.result_url, + 'gif_url': task.gif_url, + 'qiniu_url': task.qiniu_url, + 'progress': task.progress, + 'error_message': task.error_message, + }) + + response_data = { + "video_tasks": task_list, + "total_tasks": paginator.count, + "total_pages": paginator.num_pages, + "current_page": tasks_page.number, + } + + return JsonResponse(response_data, status=200) + except User.DoesNotExist: + return JsonResponse({"message": "用户不存在"}, status=404) + else: + return JsonResponse({"message": "非法参数"},status=400) + + +class DeleteVideoTaskView(View): + def delete(self, request, user_id, task_id): + x = request.headers.get('X', '') + t = request.headers.get('T', '') + + if decrypt_param(x, t): + try: + task = VideoTask.objects.get(nickname=user_id, task_id=task_id) + task.delete() + return JsonResponse({"message": "删除成功"}, status=200) + except User.DoesNotExist: + return JsonResponse({"message": "用户不存在"}, status=404) + except VideoTask.DoesNotExist: + return JsonResponse({"message": "任务不存在"}, status=404) + except Exception as e: + return JsonResponse({"message": f"删除失败: {str(e)}"}, status=500) + else: + return JsonResponse({"message": "非法参数"},status=400) + + +class AssetLibraryView(View): + def get(self, request): + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + search_query = request.GET.get('search', '') + category = request.GET.get('category', '') + page = request.GET.get('page', 1) + page_size = request.GET.get('page_size', 10) + + # 过滤条件 + assets = AssetLibrary.objects.filter(is_approved=True).order_by('-generated_at') + if search_query: + assets = assets.filter(description__icontains=search_query) + if category: + assets = assets.filter(category=category) + + paginator = Paginator(assets, page_size) + try: + assets_page = paginator.page(page) + except PageNotAnInteger: + assets_page = paginator.page(1) + except EmptyPage: + assets_page = paginator.page(paginator.num_pages) + + asset_list = [] + for asset in assets_page: + asset_list.append({ + "asset_id": asset.id, + 'qiniu_url': asset.qiniu_url, + 'gif_url': asset.gif_url, + 'original_url': asset.original_url, + 'duration': asset.duration, + 'category': asset.category, + 'description': asset.description, + 'generated_at': asset.generated_at.strftime('%Y-%m-%d %H:%M:%S'), + 'download_count': asset.download_count, + 'is_approved': asset.is_approved, + }) + + response_data = { + 'total_count': paginator.count, + 'total_pages': paginator.num_pages, + 'current_page': assets_page.number, + 'assets': asset_list + } + + return JsonResponse(response_data, status=200) + else: + return JsonResponse({"message": "非法参数"},status=400) + + + +#统计素材库下载 保存视频 复制链接 +def increment_download_count(request): + if request.method == 'POST': + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + data = json.loads(request.body) + asset_id = data.get('asset_id','') + openid = data.get('openid') + increment = -1 + function_type = 'increment_download_count' + if not asset_id: + result = update_usage_count(openid, increment, function_type) + if result['success']: + return JsonResponse({'success': True, 'message': 'ok'}, status=200) + else: + logger.error(f' 视频号提取-复制文案-次数不足 {result}') + return JsonResponse(result) + else: + asset = get_object_or_404(AssetLibrary, id=asset_id) + asset.download_count += 1 + asset.save() + result = update_usage_count(openid, increment, function_type) + if result['success']: + return JsonResponse({'success': True, 'message': 'ok'}, status=200) + else: + logger.error(f' 次数不足 {result}') + return JsonResponse(result) + else: + return JsonResponse({'error': '非法参数'}, status=400) + return JsonResponse({'success': False, 'message': 'no'}, status=405) + + +headers = { + "accept": "application/json", + "content-type": "application/json", + "Authorization": "12e76710fad2047db8c0cc6b25987e2a2" # 替换为你的真实授权密钥 +} +callback_url = "http://5109tb4417.qicp.vip/myapp/video-generation-callback/" +class GenerateVideoView(View): + def post(self, request): + data = json.loads(request.body) + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + user_id = data.get('user_id') + text_prompt = data.get('text_prompt') + width = data.get('width') + height = data.get('height') + motion_score = data.get('motion_score') + style = data.get('style', '') + description_en = translate(text_prompt, 'auto', 'en') + # 添加风格到描述末尾 + if style: + description_en += f".style: {style}" + + try: + user = User.objects.get(nickname=user_id) + except User.DoesNotExist: + return JsonResponse({"message": "用户不存在"}, status=404) + + # 判断用户是否有使用次数 + increment = -10 # 假设每次生成视频需要消耗一次使用次数 + function_type = 'GenerateVideoView' + result = update_usage_count(user.openid, increment, function_type) + if not result['success']: + return JsonResponse(result) + + payload = { + "text_prompt": description_en, + "width": width, + "height": height, + "motion": motion_score, + "seed": 0, + "upscale": True, + "interpolate": True, + "callback_url": callback_url + } + + try: + response = requests.post("https://api.aivideoapi.com/runway/generate/text", json=payload, headers=headers) + response.raise_for_status() # 如果响应状态码不是 200,抛出 HTTPError 异常 + except requests.exceptions.RequestException as e: + logger.error(f"请求视频生成接口时出错: {e}") + return JsonResponse({"message": "任务创建失败"}, status=500) + + response_data = response.json() + if 'uuid' in response_data: + task_id = response_data['uuid'] + VideoTask.objects.create( + openid=user.openid, + nickname=user.nickname, + task_id=task_id, + task_type='文生视频', + status='running', + text_prompt=description_en, + description_zh=text_prompt, + width=width, + height=height, + motion_score=motion_score, + style=style, + seconds=4 + ) + return JsonResponse({"message": "任务创建成功", "task_id": task_id}, status=200) + else: + logger.error(f"任务创建失败,响应: {response_data}") + return JsonResponse({"message": "任务创建失败"}, status=500) + else: + return JsonResponse({"message": "非法参数"}, status=400) + + +class ExtendVideoView(View): + def post(self, request): + data = json.loads(request.body) + x = request.headers.get('X', '') + t = request.headers.get('T', '') + if decrypt_param(x, t): + user_id = data.get('user_id') + task_id = data.get('task_id') + motion_score = data.get('motion_score', 5) + + try: + # 从数据库中获取任务信息 + task = VideoTask.objects.get(task_id=task_id) + user = User.objects.get(nickname=user_id) + except VideoTask.DoesNotExist: + return JsonResponse({"message": "任务不存在"}, status=404) + except User.DoesNotExist: + return JsonResponse({"message": "用户不存在"}, status=404) + + # 判断用户是否有使用次数 + increment = -10 # 假设每次生成视频需要消耗一次使用次数 + function_type = 'ExtendVideoView' + result = update_usage_count(user.openid, increment, function_type) + if not result['success']: + return JsonResponse(result) + + # 计算当前总时长 + current_seconds = int(task.seconds) + + # 确保扩展后的总时长不超过16秒 + additional_seconds = 4 # 每次增加4秒 + if current_seconds + additional_seconds > 16: + return JsonResponse({"message": "扩展后的总时长不能超过16秒"}, status=400) + + new_total_seconds = current_seconds + additional_seconds + + payload = { + "uuid": task_id, + "motion": motion_score, + "seed": 0, + "upscale": True, + "interpolate": True, + "callback_url": callback_url + } + + try: + response = requests.post("https://api.aivideoapi.com/runway/extend", json=payload, headers=headers) + response.raise_for_status() # 如果响应状态码不是 200,抛出 HTTPError 异常 + except requests.exceptions.RequestException as e: + logger.error(f"请求视频扩展接口时出错: {e}") + return JsonResponse({"message": "任务创建失败"}, status=500) + + response_data = response.json() + if 'uuid' in response_data: + new_task_id = response_data['uuid'] + VideoTask.objects.create( + openid=task.openid, + nickname=task.nickname, + task_id=new_task_id, + task_type='文生视频', + status='running', + text_prompt=task.text_prompt, + description_zh=task.description_zh, + width=task.width, + height=task.height, + motion_score=motion_score, + style=task.style, + seconds=new_total_seconds, + ) + return JsonResponse({"message": "任务创建成功", "task_id": new_task_id}, status=200) + else: + logger.error(f"任务创建失败,响应: {response_data}") + return JsonResponse({"message": "任务创建失败"}, status=500) + else: + return JsonResponse({"message": "非法参数"}, status=400) + +class GenerateImageVideoView(View): + def post(self, request): + data = json.loads(request.body) + user_id = data.get('user_id') + text_prompt = data.get('text_prompt') + image_url = data.get('image_url') + motion_score = data.get('motion_score', 5) # M 1-10,默认值为5 + description_en = translate(text_prompt, 'auto', 'en') + + try: + user = User.objects.get(nickname=user_id) + except User.DoesNotExist: + return JsonResponse({"message": "用户不存在"}, status=404) + + # 判断用户是否有使用次数 + increment = -10 # 假设每次生成视频需要消耗一次使用次数 + function_type = 'GenerateImageVideoView' + result = update_usage_count(user.openid, increment, function_type) + if not result['success']: + return JsonResponse(result) + + payload = { + "text_prompt": description_en, + "img_prompt": image_url, + "motion": motion_score, + "seed": 0, + "upscale": True, + "interpolate": True, + "callback_url": callback_url + } + + try: + response = requests.post("https://api.aivideoapi.com/runway/generate/imageDescription", json=payload, headers=headers) + response.raise_for_status() # 如果响应状态码不是 200,抛出 HTTPError 异常 + except requests.exceptions.RequestException as e: + logger.error(f"请求视频生成接口时出错: {e}") + return JsonResponse({"message": "任务创建失败"}, status=500) + + response_data = response.json() + if 'uuid' in response_data: + task_id = response_data['uuid'] + VideoTask.objects.create( + openid=user.openid, + nickname=user.nickname, + task_id=task_id, + task_type='图生视频', + status='running', + text_prompt=description_en, + description_zh=text_prompt, + image_url=image_url, + motion_score=motion_score, + seconds=4 + + ) + return JsonResponse({"message": "任务创建成功", "task_id": task_id}, status=200) + else: + logger.error(f"任务创建失败,响应: {response_data}") + return JsonResponse({"message": "任务创建失败"}, status=500) + + +class TaskStatusView(View): + def get(self, request, task_id): + if not task_id: + return JsonResponse({"message": "缺少任务ID"}, status=400) + + try: + # 从数据库中获取任务 + task = VideoTask.objects.get(task_id=task_id) + except VideoTask.DoesNotExist: + return JsonResponse({"message": "任务不存在"}, status=404) + url = f"https://api.aivideoapi.com/status?uuid={task_id}" + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + response_data = response.json() + print(response_data) + except requests.exceptions.RequestException as e: + logger.error(f"查询任务状态时出错: {e}") + return JsonResponse({"message": "查询任务状态失败", "error": str(e)}, status=500) + progress = float(response_data.get('progress', 0)) if response_data.get('progress') is not None else 0.0 + task.progress = progress + if task.progress >= 1.0: + if response_data.get('status', task.status) == 'success': + task.status = 'success' + task.result_url = response_data.get('url', task.result_url) + task.gif_url = response_data.get('gif_url', task.gif_url) + AssetLibrary.objects.create( + original_url=task.result_url, + duration=task.seconds, + category=task.style, + description=task.text_prompt, + description_zh=task.description_zh, + generated_by=task.nickname, + gif_url=task.gif_url, + is_approved=True # 假设任务成功的素材自动审核通过 + ) + elif response_data.get('status', task.status) == 'failed': + task.status = 'failed' + elif progress==0.0: + task.status = 'pending' + else: + task.status = 'running' + + # 保存更新后的任务 + task.save() + + return JsonResponse({ + "task_id": task.task_id, + "status": task.status, + "progress": task.progress, + "result_url": task.result_url, + "gif_url": task.gif_url + }, status=200) + + +class UpdatePendingTasksView(View): + def get(self, request): + pending_tasks = VideoTask.objects.filter(status__in=['running', 'pending']) + task_updates = [] + # 打印当前执行时间和任务数量 + current_time = timezone.now().strftime('%Y-%m-%d %H:%M:%S') + logger.info(f"当前执行时间:{current_time}, 共 {len(pending_tasks)} 个任务") + + for task in pending_tasks: + task_info = { + "task_id": task.task_id, + "status": "正在执行", + "message": "" + } + logger.info(f"{task_info}") + + try: + # 调用 QueryTaskStatusView 接口 + response = requests.get(f"http://127.0.0.1:55556/myapp/task_status/{task.task_id}/") + if response.status_code == 200: + task_data = response.json() + print(task_data) + task_info = { + "task_id": task.task_id, + "status": "执行成功", + "message": "" + } + logger.info(f"{task_info}") + task_updates.append(task_info) + else: + task_info["status"] = "执行失败" + task_info["message"] = f"请求失败,状态码:{response.status_code}" + logger.error(f"{task_info}") + task_updates.append(task_info) + except Exception as e: + task_info["status"] = "执行失败" + task_info["message"] = f"错误信息:{str(e)}" + logger.error(f"{task_info}") + task_updates.append(task_info) + + return JsonResponse({"updated_tasks": task_updates}, status=200) + + + +#每日奖励次数 +class DailyCoinsBonusView(View): + def get(self, request): + daily_bonus = int(request.GET.get('daily_bonus', 5)) # 获取每日赠送的金币数量,默认为5 + + try: + # 获取所有用户 + users = User.objects.all() + users_count = users.count() + + # 为所有用户添加每日金币奖励 + for user in users: + user.coins += daily_bonus + user.save() + + logger.info(f"总用户数量:{users_count}, 每个用户已赠送 {daily_bonus} 个金币") + return JsonResponse({"message": "每日金币赠送成功"}, status=200) + + except Exception as e: + logger.error(f"每日赠送金币时出错: {str(e)}") + return JsonResponse({"message": "每日赠送金币时出错"}, status=500) + +class VideoGenerationCallbackView(View): + def get(self, request): + try: + print(request) + print(request.body) + data = request.GET.dict() + logger.info(f'收到视频生成结果回调: {data}') + print(f'收到视频生成结果回调: {data}') # 直接打印到控制台 + + task_id = data.get('uuid') + status = data.get('status') + progress = float(data.get('progress', 0)) + result_url = data.get('url') + gif_url = data.get('gif_url') + + if not task_id: + return JsonResponse({'success': False, 'message': '缺少任务ID'}, status=400) + + try: + task = VideoTask.objects.get(task_id=task_id) + except VideoTask.DoesNotExist: + return JsonResponse({'success': False, 'message': '任务不存在'}, status=404) + + task.progress = progress + + if status == 'success' and progress == 1.0: + task.status = 'success' + task.result_url = result_url + task.gif_url = gif_url + elif status == 'failed': + task.status = 'failed' + else: + task.status = 'running' + + task.save() + + return JsonResponse({'success': True, 'message': '回调接收成功'}, status=200) + except json.JSONDecodeError as e: + logger.error(f'无效的请求数据: {str(e)}') + return JsonResponse({'success': False, 'message': '无效的请求数据'}, status=400) + except Exception as e: + logger.error(f'处理回调时出错: {str(e)}') + return JsonResponse({'success': False, 'message': '服务器内部错误'}, status=500) + + def post(self, request): + return JsonResponse({'success': False, 'message': '请求方法不支持'}, status=405) \ No newline at end of file diff --git a/MyApi/wxpay.py b/MyApi/wxpay.py new file mode 100644 index 0000000..fe5bfd5 --- /dev/null +++ b/MyApi/wxpay.py @@ -0,0 +1,310 @@ +import base64 +import json +import string +from datetime import datetime, timedelta +from decimal import Decimal + +import requests +import time +import random +from xml.etree import ElementTree as ET +from Crypto.Hash import SHA256 +from Crypto.PublicKey import RSA +from Crypto.Signature import pkcs1_15 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from django.http import JsonResponse, HttpResponse +from django.views.decorators.csrf import csrf_exempt +from django.conf import settings +from .models import TransactionLog, MembershipType,User +from .API_Log import get_logger +log_file = '支付日志.log' +logger = get_logger('支付日志', log_file, when='midnight', backup_count=7) + + +def wx_pay(request): + request_data = json.loads(request.body) + type = request_data.get('type', 'default') + wx_price = request_data['total_fee'] # 从请求获取订单金额 + openid = request_data['openid'] # 从请求获取用户openid + transaction_type = request_data['transaction_type'] # 续费或者开通 + up_time = datetime.now() + + # 从数据库中动态获取 body_map + membership_types = MembershipType.objects.all() + body_map = {membership_type.type: membership_type.title for membership_type in membership_types} + + body_description = body_map.get(type) + + # 以交易日期生成交易号 + transactionNo = str(up_time).replace('.', '').replace('-', '').replace(':', '').replace(' ', '') + + membership_type = MembershipType.objects.get(type=type) + price = membership_type.price + newTransaction = TransactionLog.objects.create( + transaction_no=transactionNo, + transaction_status='pending', + user_openid=openid, + transaction_type=transaction_type, + transaction_amount=price, + remark=type, + created_at=up_time + ) + + # 打印和记录日志 + logger.info("生成订单:") + logger.info(f"商户订单号:{transactionNo}") + logger.info(f"用户OpenID:{openid}") + logger.info(f"type:{type}") + logger.info(f"transaction_type:{transaction_type}") + logger.info(f"成功时间:{up_time}") + logger.info(f"金额(前端):{wx_price}") + logger.info(f"金额(后端):{price}") + + # 生成统一下单的报文body + url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi' + body = { + "appid": settings.WX_APP_ID, + "mchid": settings.WX_MCH_ID, + "description": body_description, + "out_trade_no": transactionNo, + "notify_url": settings.WX_NOTIFY_URL, # 后端接收回调通知的接口 + "amount": {"total": int(float(price) * 100), "currency": "CNY"}, # 微信金额单位为分 + "payer": {"openid": openid}, + "attach": json.dumps({"type": type, "transaction_type": transaction_type}) + } + data = json.dumps(body) + + # 定义生成签名的函数 + def get_sign(sign_str): + rsa_key = RSA.importKey(open(settings.WX_KEY_PATH).read()) + signer = pkcs1_15.new(rsa_key) + digest = SHA256.new(sign_str.encode('utf8')) + sign = base64.b64encode(signer.sign(digest)).decode('utf-8') + return sign + + # 生成请求随机串 + random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32)) + + # 生成请求时间戳 + time_stamps = str(int(time.time())) + + # 生成签名串 + sign_str = f"POST\n/v3/pay/transactions/jsapi\n{time_stamps}\n{random_str}\n{data}\n" + + # 生成签名 + sign = get_sign(sign_str) + + # 生成HTTP请求头 + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': '*/*', + 'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{settings.WX_MCH_ID}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{settings.WX_SERIAL_NO}"', + 'Wechatpay-Serial': '66DB35F836EFD4CBEC66F3815D283A2892310324' + } + + # 发送请求获得prepay_id + response = requests.post(url, data=data, headers=headers) # 获取预支付交易会话标识(prepay_id) + + # 应答签名验证 + wechatpaySerial = response.headers['Wechatpay-Serial'] # 获取HTTP头部中包括回调报文的证书序列号 + wechatpaySignature = response.headers['Wechatpay-Signature'] # 获取HTTP头部中包括回调报文的签名 + wechatpayTimestamp = response.headers['Wechatpay-Timestamp'] # 获取HTTP头部中包括回调报文的时间戳 + wechatpayNonce = response.headers['Wechatpay-Nonce'] # 获取HTTP头部中包括回调报文的随机串 + + # 获取微信平台证书 + url2 = "https://api.mch.weixin.qq.com/v3/certificates" + + # 生成证书请求随机串 + random_str2 = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32)) + + # 生成证书请求时间戳 + time_stamps2 = str(int(time.time())) + + # 生成请求证书的签名串 + data2 = "" + sign_str2 = f"GET\n/v3/certificates\n{time_stamps2}\n{random_str2}\n{data2}\n" + + # 生成签名 + sign2 = get_sign(sign_str2) + + # 生成HTTP请求头 + headers2 = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' + f'mchid="{settings.WX_MCH_ID}",nonce_str="{random_str2}",signature="{sign2}",timestamp="{time_stamps2}",serial_no="{settings.WX_SERIAL_NO}"' + } + + # 发送请求获得证书 + response2 = requests.get(url2, headers=headers2) # 只需要请求头 + cert = response2.json() + + # 证书解密 + def decrypt(nonce, ciphertext, associated_data): + key = settings.WX_API_KEY + key_bytes = str.encode(key) + nonce_bytes = str.encode(nonce) + ad_bytes = str.encode(associated_data) + data = base64.b64decode(ciphertext) + aesgcm = AESGCM(key_bytes) + return aesgcm.decrypt(nonce_bytes, data, ad_bytes) + + nonce = cert["data"][0]['encrypt_certificate']['nonce'] + ciphertext = cert["data"][0]['encrypt_certificate']['ciphertext'] + associated_data = cert["data"][0]['encrypt_certificate']['associated_data'] + serial_no = cert["data"][0]['serial_no'] + certificate = decrypt(nonce, ciphertext, associated_data) + + # 签名验证 + if wechatpaySerial == serial_no: # 应答签名中的序列号同证书序列号应相同 + print('serial_no match') + + def verify(data, signature): # 验签函数 + key = RSA.importKey(certificate) # 直接使用解密后的证书 + verifier = pkcs1_15.new(key) + hash_obj = SHA256.new(data.encode('utf8')) + return verifier.verify(hash_obj, base64.b64decode(signature)) + + data3 = f"{wechatpayTimestamp}\n{wechatpayNonce}\n{response.text}\n" + try: + verify(data3, wechatpaySignature) + + # 生成调起支付API需要的参数并返回前端 + res = { + 'timeStamp': time_stamps, + 'nonceStr': random_str, + 'package': 'prepay_id=' + response.json()['prepay_id'], + 'paySign': get_sign( + f"{settings.WX_APP_ID}\n{time_stamps}\n{random_str}\n{'prepay_id=' + response.json()['prepay_id']}\n"), + } + print(f'签名有效: {res}') + logger.info(f'签名有效: {res}') + return HttpResponse(json.dumps(res), content_type='application/json') + except (ValueError, TypeError): + logger.info(f'签名无效') + print('签名无效') + return HttpResponse(json.dumps({'msg': "支付失败"}), content_type='application/json') + +def wx_resource(nonce, ciphertext, associated_data): + key_bytes = str.encode(settings.WX_API_KEY) # APIv3_key(商户平台设置) + nonce_bytes = str.encode(nonce) + ad_bytes = str.encode(associated_data) + data = base64.b64decode(ciphertext) + aesgcm = AESGCM(key_bytes) + plaintext = aesgcm.decrypt(nonce_bytes, data, ad_bytes) + plaintext_str = bytes.decode(plaintext) + + return eval(plaintext_str) + +def update_user_membership_or_quota(openid, membership_type): + current_time = int(time.time()) # 获取当前时间戳 + + try: + user = User.objects.get(openid=openid) + + if membership_type.is_quota: + # 如果是额度充值,更新用户的金币数量 + user.coins += membership_type.coins + logger.info(f'用户 {user.id} (openid: {openid}) 充值额度卡,增加金币 {membership_type.coins}') + else: + # 如果是会员充值,更新会员状态和时间 + user.is_member = True + + if user.member_start_time is None: # 如果用户是第一次开会员 + user.member_start_time = current_time + logger.info(f'第一次开会员:{current_time} -- {current_time + membership_type.duration_days * 24 * 3600}') + elif user.member_end_time is None or user.member_end_time < current_time: # 如果会员已经过期 + logger.info(f'过期:{current_time} -- {current_time + membership_type.duration_days * 24 * 3600}') + user.member_start_time = current_time + + # 计算会员到期时间 + if user.member_end_time is None or user.member_end_time < current_time: + logger.info(f'第一次开会员2:{user.member_end_time} -- {current_time + membership_type.duration_days * 24 * 3600}') + user.member_end_time = current_time + membership_type.duration_days * 24 * 3600 # 将天数转换为秒 + else: # 如果会员尚未过期,则在现有会员结束时间上添加续费的天数 + logger.info(f'续费:{user.member_end_time} -- {user.member_end_time + membership_type.duration_days * 24 * 3600}') + user.member_end_time += membership_type.duration_days * 24 * 3600 + + # 会员充值赠送金币 + user.coins += membership_type.coins + + user.save() + return True + except User.DoesNotExist: + # 处理用户不存在的情况 + logger.error(f'用户 {openid} 不存在') + return False + except Exception as e: + logger.error(f'更新会员或额度充值时出错: {str(e)}') + return False + + +@csrf_exempt +def wx_pay_notify(request): + webData = json.loads(request.body) + logger.info(f'回调返回信息:{webData}') + ciphertext = webData['resource']['ciphertext'] + nonce = webData['resource']['nonce'] + associated_data = webData['resource']['associated_data'] + try: + callback_data = wx_resource(nonce, ciphertext, associated_data) + logger.info(f'回调返回信息:{callback_data}') + mchid = callback_data.get('mchid') + appid = callback_data.get('appid') + out_trade_no = callback_data.get('out_trade_no') + transaction_id = callback_data.get('transaction_id') + trade_state = callback_data.get('trade_state') + openid = callback_data.get('payer').get('openid') + attach = callback_data.get('attach') + attach = attach.replace("'", "\"") + attach = json.loads(attach) + type = attach.get('type') + transaction_type = attach.get('transaction_type') + success_time_str = callback_data.get('success_time') + amount_total = callback_data.get('amount').get('total') + success_time = datetime.strptime(success_time_str, "%Y-%m-%dT%H:%M:%S%z") + logger.info("交易结果:") + logger.info(f"商户号:{mchid}") + logger.info(f"AppID:{appid}") + logger.info(f"商户订单号:{out_trade_no}") + logger.info(f"微信订单号:{transaction_id}") + logger.info(f"交易状态:{trade_state}") + logger.info(f"用户OpenID:{openid}") + logger.info(f"type:{type}") + logger.info(f"transaction_type:{transaction_type}") + logger.info(f"成功时间:{success_time}") + logger.info(f"金额(分):{amount_total}") + transaction_log = TransactionLog.objects.get(transaction_no=out_trade_no) + membership_type = MembershipType.objects.get(type=type) # 获取会员类型对象 + + # 验证订单信息一致性 + if (transaction_log.user_openid == openid) and (transaction_log.transaction_amount * 100 == amount_total) and ( + membership_type.price * 100 == amount_total): + # 更新交易状态为完成 + transaction_log.transaction_status = 'completed' + transaction_log.save() + logger.info(f'交易记录更新成功:{transaction_log}') + + # 更新用户的会员或额度 + success = update_user_membership_or_quota(openid, membership_type) + + # 给邀请人返利 + user = User.objects.get(openid=openid) + if user.inviter_nickname: + inviter = User.objects.filter(nickname=user.inviter_nickname).first() + if inviter: + rebate_amount = round(Decimal(amount_total) * Decimal(0.35) / Decimal(100), 2) # 保留两位小数,单位为元 + inviter.balance += rebate_amount + inviter.save() + logger.info(f'邀请人{inviter.nickname}返利{rebate_amount}元成功') + return HttpResponse(json.dumps({"code": "SUCCESS", "message": "成功"})) + else: + logger.warning('回调数据与订单记录不一致') + return HttpResponse(json.dumps({"code": "FAIL", "message": "回调数据与订单记录不一致"})) + except TransactionLog.DoesNotExist: + logger.error(f'交易记录不存在') + return HttpResponse(json.dumps({"code": "FAIL", "message": "交易记录不存在"})) + except Exception as e: + logger.error(f'处理回调时出错:{str(e)}') + return HttpResponse(json.dumps({"code": "FAIL", "message": "处理回调时出错"})) + diff --git a/MyApi/证书/apiclient_cert.pem b/MyApi/证书/apiclient_cert.pem new file mode 100644 index 0000000..42c107f --- /dev/null +++ b/MyApi/证书/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIENzCCAx+gAwIBAgIUZts1+Dbv1MvsZvOBXSg6KJIxAyQwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjQwNDIwMDk0OTU5WhcNMjkwNDE5MDk0OTU5WjCBkDETMBEGA1UEAwwK +MTY3NDAxMjkzMjEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTwwOgYDVQQL +DDPnpo/lt57luILpvJPmpbzljLrlpKnov5zkuablupfvvIjkuKrkvZPlt6XllYbm +iLfvvIkxCzAJBgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAK3ueF4X+IVGCSajTvZycizthPIoxspbCTHu +5XzLRDAiNL92w6sJIb488MKIhDJtdxw4RJjtf6W9qjv1D2wI2EldehTJfJkyTFCW +xQlPh615dMDNayg/L4w6cIZLcjvbIl/RGAoZ7ldr5Z6+O+tFpejg9Sh4V7pU0Vhl +ZlHolaCqVbuqcRIlybF9BpPpWJRu0jPFrMG5rDpzE6HnbNiAKpdc+8mZriHgeJI7 +unfd3zQj/Qy7j3cuASjhlXZQdA/IZvy/El7n46VDk4hZCfNHu5h1CJ/S89h+BSlS +WBvCiZZAiKZS7ELjlHsA7bZf50LYokPyvDgx628dVszsVy0+GsECAwEAAaOBuTCB +tjAJBgNVHRMEAjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGH +hoGEaHR0cDovL2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0x +QkQ0MjIwRTUwREJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0 +NzFCNjU0MjJFMTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3 +DQEBCwUAA4IBAQCvG+nV62rfX5kVxO8Uc8AL9UbpXIWi+6qL0+tBfR5YQWsYDY9n +wMLoYYAwcQ+xTurQcsUA4WOFrhgkrelcyWu6jhBKhVBLEed2jL5pKLwt+CvCvTYk +4Q1MW6f7P1Gs+/OMAleu55YGMCe1PqvtTypljT+mx8rHXtgLSNDtCO65jXRbbehs +kxQalLrzGEeUaLk4UknDZJInALNZHeqT9k8NXBZrlIlM9V4oPb+Ai/FCXkO/kqHS +b0OORRBgi74sLogXyQSma4uHhyNDGrmGSTBHu3If6l9QhEHSZjTQeXIK7wbBGgAw +LDd8xm4cEWxi6xyssl0s/7qs+pG/eG47MP9J +-----END CERTIFICATE----- diff --git a/MyApi/证书/apiclient_key.pem b/MyApi/证书/apiclient_key.pem new file mode 100644 index 0000000..4092626 --- /dev/null +++ b/MyApi/证书/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCt7nheF/iFRgkm +o072cnIs7YTyKMbKWwkx7uV8y0QwIjS/dsOrCSG+PPDCiIQybXccOESY7X+lvao7 +9Q9sCNhJXXoUyXyZMkxQlsUJT4eteXTAzWsoPy+MOnCGS3I72yJf0RgKGe5Xa+We +vjvrRaXo4PUoeFe6VNFYZWZR6JWgqlW7qnESJcmxfQaT6ViUbtIzxazBuaw6cxOh +52zYgCqXXPvJma4h4HiSO7p33d80I/0Mu493LgEo4ZV2UHQPyGb8vxJe5+OlQ5OI +WQnzR7uYdQif0vPYfgUpUlgbwomWQIimUuxC45R7AO22X+dC2KJD8rw4MetvHVbM +7FctPhrBAgMBAAECggEAOgEtYzPbTZbttlUAIHBKY3FSxO+UXCfACUcCgXvIYcUG +klOpLYD+H9Ny921PqQGYl3Csb9PEniGChDxVyFGqz8y8yfHn+68qhDXDwDclqFS4 ++xOGiQWJddqHbEH89rk93XZ97eB3+++fxDDtCqlPizp6h+SaXSmsJy6p0Ocf/1RC +IFXUz3J3DsKAHySklq4GyovSOWd6o/KzJiKfRDwn85Jc5uCXyM7rzVfXUWx1HOsr +tPwXTAxE8opUsIP7BnAErgrGRZJc6gQunfbye5JQYxEeg1Hxkc+HbJHhbjueWMDW +cMzappDQH4NL0QeYDRfhkeV4/glQHJhDegVmyAPlgQKBgQDcttEMVlNgWzKTY7Wv +BoqUxH6ZMpvNDRehCqpIqoXYNodCKT66vYBXEfAGxIdIyDE+bEWKpSwSNX4l9Hay +d52eKv73f7Go0Uo54SwMlsNBKA3xZ1terskJlsDwtvT7N387GUyZpv6Ej/qJLIIk +WsuNgLKH8PQklaN8afnIIy2OvwKBgQDJvPqBKG8CP4CfxbKue063tkZdVjffyv+4 +w/Y81lMCu5tvfebF7drUVSYqaLSXD71ramViDe++PQxksqmk/H0dLQZVJuNdunk/ +w/iyjk/dI+wh2dILL7DwX6vaFiA+S6YjXWJkv148tHL0+tVotUOh9ETzBN8oO5R2 +2Ik5zHg2fwKBgGD7X74ZDIfRatbCwGmI6UnXUX2FdFpUf2Z+5jYJ38gSpbpXnz20 +2PjpY9vFbWB4vtKcBq6WR1g08xpAYgunbbW7VM9x56GzXPczzwQ1FtTBgebqMIbZ +LHMvL9ZVvi3Iw/o28qatbzbWgRLXYNQSpVCnFUuQf+a3bd/UPTJI6tLHAoGBAKbi +I8AQeLQ9X+PLoWJhMt8ac97mYdZJkX2A7gOpsIXEOHLkjbEScEHSJn5Sm4GTTbi8 +DR4uNTMEQWpTua54B+/IvUJXpyJ0DbVIIkVjIIHpI39lNzJ1w0M1gW4pe4bNAaSj +ra919zj0dVxe24eA22/wJ9F5KDmZ/9jIi8w6bv91AoGAefCgs6tA9cA3dag9lWP+ +8+S4rpXSR7qCTmSfxCvWkq13vh4jlIFhChBpu+re91n9vyIUWgrlCcJUwft2WBqF +60Huafsy3hQTuclm15sYtEkeuh9OKuzUKJ3mfTXYef6F8RKOOseuFtXnH5xmZEJl +AifcseiIEMcIlBIlIjRZlSs= +-----END PRIVATE KEY-----