From d91da23a3f36a43dfab5a861b8eb93b5e30f3a4f Mon Sep 17 00:00:00 2001 From: rogersun Date: Tue, 9 Jun 2026 11:58:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0AI=E6=8E=92=E7=89=87=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- ai/__init__.py | 0 ai/admin.py | 3 + ai/apps.py | 6 + ai/migrations/0001_initial.py | 51 +++++ ...02_aishow_take_times_aishow_take_tokens.py | 23 +++ ai/migrations/0003_prompttemplate.py | 28 +++ ai/migrations/__init__.py | 0 ai/models.py | 59 ++++++ ai/serializers.py | 21 ++ ai/tasks.py | 10 + ai/tests.py | 3 + ai/urls.py | 25 +++ ai/utils/__init__.py | 0 ai/utils/datetime_format.py | 37 ++++ ai/utils/show_database.py | 136 +++++++++++++ ai/utils/show_process.py | 54 ++++++ ai/utils/show_prompt.py | 130 +++++++++++++ ai/utils/sql.py | 182 ++++++++++++++++++ ai/views.py | 75 ++++++++ dingxin_toolbox_drf/settings.py | 2 + dingxin_toolbox_drf/urls.py | 1 + reqirement_linux.txt | Bin 3532 -> 1942 bytes update/migrations/0034_cinema_url.py | 18 ++ 24 files changed, 868 insertions(+), 1 deletion(-) create mode 100644 ai/__init__.py create mode 100644 ai/admin.py create mode 100644 ai/apps.py create mode 100644 ai/migrations/0001_initial.py create mode 100644 ai/migrations/0002_aishow_take_times_aishow_take_tokens.py create mode 100644 ai/migrations/0003_prompttemplate.py create mode 100644 ai/migrations/__init__.py create mode 100644 ai/models.py create mode 100644 ai/serializers.py create mode 100644 ai/tasks.py create mode 100644 ai/tests.py create mode 100644 ai/urls.py create mode 100644 ai/utils/__init__.py create mode 100644 ai/utils/datetime_format.py create mode 100644 ai/utils/show_database.py create mode 100644 ai/utils/show_process.py create mode 100644 ai/utils/show_prompt.py create mode 100644 ai/utils/sql.py create mode 100644 ai/views.py create mode 100644 update/migrations/0034_cinema_url.py diff --git a/.gitignore b/.gitignore index e2000d4..e1ddef0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ /dx/ /logs/ /logs/dingxin.log -*.log \ No newline at end of file +*.log +/celerybeat-schedule.bak +/celerybeat-schedule.dat +/celerybeat-schedule.dir diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/admin.py b/ai/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/ai/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/ai/apps.py b/ai/apps.py new file mode 100644 index 0000000..9c56fc5 --- /dev/null +++ b/ai/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ShowAiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'ai' diff --git a/ai/migrations/0001_initial.py b/ai/migrations/0001_initial.py new file mode 100644 index 0000000..d70b5b0 --- /dev/null +++ b/ai/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.7 on 2026-06-08 07:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AiShow', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('cinema', models.CharField(max_length=50)), + ('zz_code', models.CharField(max_length=50)), + ('show_date', models.DateField()), + ('is_ai_show', models.BooleanField(default=True)), + ('show', models.TextField()), + ('sales', models.CharField(max_length=50)), + ('prompt', models.TextField()), + ('result', models.TextField()), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'AI排片数据', + 'verbose_name_plural': 'AI排片数据', + 'db_table': 'ai_show_result', + }, + ), + migrations.CreateModel( + name='TestCinema', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=50)), + ('zz_code', models.CharField(max_length=50)), + ('db_config', models.TextField()), + ('user_config', models.TextField()), + ('is_active', models.BooleanField(default=True)), + ], + options={ + 'verbose_name': '测试影院', + 'verbose_name_plural': '测试影院', + 'db_table': 'ai_show_cinema', + }, + ), + ] diff --git a/ai/migrations/0002_aishow_take_times_aishow_take_tokens.py b/ai/migrations/0002_aishow_take_times_aishow_take_tokens.py new file mode 100644 index 0000000..a67fa47 --- /dev/null +++ b/ai/migrations/0002_aishow_take_times_aishow_take_tokens.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2026-06-08 10:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ai', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='aishow', + name='take_times', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='aishow', + name='take_tokens', + field=models.CharField(default='', max_length=2000), + ), + ] diff --git a/ai/migrations/0003_prompttemplate.py b/ai/migrations/0003_prompttemplate.py new file mode 100644 index 0000000..53299e1 --- /dev/null +++ b/ai/migrations/0003_prompttemplate.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.7 on 2026-06-09 02:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ai', '0002_aishow_take_times_aishow_take_tokens'), + ] + + operations = [ + migrations.CreateModel( + name='PromptTemplate', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('prompt_type', models.CharField(max_length=50)), + ('prompt_key', models.CharField(max_length=50)), + ('prompt_val', models.TextField()), + ('del_flag', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': '提示词', + 'verbose_name_plural': '提示词', + 'db_table': 'ai_prompt_template', + }, + ), + ] diff --git a/ai/migrations/__init__.py b/ai/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/models.py b/ai/models.py new file mode 100644 index 0000000..93867c1 --- /dev/null +++ b/ai/models.py @@ -0,0 +1,59 @@ +from django.db import models + + +# Create your models here. +class TestCinema(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=50) + zz_code = models.CharField(max_length=50) + db_config = models.TextField() + user_config = models.TextField() + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = '测试影院' + verbose_name_plural = '测试影院' + db_table = 'ai_show_cinema' + + +class PromptTemplate(models.Model): + id = models.AutoField(primary_key=True) + prompt_type = models.CharField(max_length=50) + prompt_key = models.CharField(max_length=50) + prompt_val = models.TextField() + del_flag = models.BooleanField(default=False) + + def __str__(self): + return self.prompt_key + + class Meta: + verbose_name = '提示词' + verbose_name_plural = '提示词' + db_table = 'ai_prompt_template' + + +class AiShow(models.Model): + id = models.AutoField(primary_key=True) + cinema = models.CharField(max_length=50) + zz_code = models.CharField(max_length=50) + show_date = models.DateField() + is_ai_show = models.BooleanField(default=True) + show = models.TextField() + sales = models.CharField(max_length=50) + prompt = models.TextField() + result = models.TextField() + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + take_times = models.IntegerField(default=0) + take_tokens = models.CharField(default='', max_length=2000) + + def __str__(self): + return self.cinema + + class Meta: + verbose_name = 'AI排片数据' + verbose_name_plural = 'AI排片数据' + db_table = 'ai_show_result' diff --git a/ai/serializers.py b/ai/serializers.py new file mode 100644 index 0000000..3d0829d --- /dev/null +++ b/ai/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from ai.models import * + + +class TestCinemaSerializer(serializers.ModelSerializer): + class Meta: + model = TestCinema + fields = '__all__' + + +class PromptTemplateSerializer(serializers.ModelSerializer): + class Meta: + model = PromptTemplate + fields = '__all__' + + +class AiShowSerializer(serializers.ModelSerializer): + class Meta: + model = AiShow + fields = '__all__' diff --git a/ai/tasks.py b/ai/tasks.py new file mode 100644 index 0000000..09dd312 --- /dev/null +++ b/ai/tasks.py @@ -0,0 +1,10 @@ +from celery import shared_task +from ai.utils.show_process import show_main_process + + + +@shared_task +def ai_show_general(): + show_main_process() + print("task success") + return True diff --git a/ai/tests.py b/ai/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/ai/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/ai/urls.py b/ai/urls.py new file mode 100644 index 0000000..564a2d6 --- /dev/null +++ b/ai/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for dingxin_toolbox_drf project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from ai.views import * + +urlpatterns = [ + path('manual_general_show', manual_general_show), + path('get_cinema_show_result', get_cinema_show_result), + path('clear', clear_lock), +] diff --git a/ai/utils/__init__.py b/ai/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/utils/datetime_format.py b/ai/utils/datetime_format.py new file mode 100644 index 0000000..0fb5681 --- /dev/null +++ b/ai/utils/datetime_format.py @@ -0,0 +1,37 @@ +import datetime +from chinese_calendar import (is_holiday, is_workday) + + +def get_data_datetime(show_date): + show_date_obj = datetime.datetime.strptime(show_date, '%Y-%m-%d') + target_start_datetime_obj = show_date_obj + datetime.timedelta(hours=6) + target_end_datetime_obj = show_date_obj + datetime.timedelta(hours=29, minutes=59, seconds=59) + data_start_datetime_obj = show_date_obj + datetime.timedelta(days=-7) + datetime.timedelta(hours=6) + data_end_datetime_obj = show_date_obj + datetime.timedelta(hours=5, minutes=59, seconds=59) + history_start = datetime.datetime.strftime(data_start_datetime_obj, '%Y-%m-%d %H:%M:%S') + history_end = datetime.datetime.strftime(data_end_datetime_obj, '%Y-%m-%d %H:%M:%S') + target_start = datetime.datetime.strftime(target_start_datetime_obj, '%Y-%m-%d %H:%M:%S') + target_end = datetime.datetime.strftime(target_end_datetime_obj, '%Y-%m-%d %H:%M:%S') + return history_start, history_end, target_start, target_end + + +def get_date_type(show_date, template_date): + show_date_obj = datetime.datetime.strptime(show_date, '%Y-%m-%d') + template_date_obj = datetime.datetime.strptime(template_date, '%Y-%m-%d') + result = dict() + # 处理参考数据日期 + history_date_list = [(show_date_obj - datetime.timedelta(days=i)) for i in range(7, 0, -1)] + history_date = '、'.join( + [f'{datetime.date.strftime(d, "%Y-%m-%d")}({"节假日" if is_holiday(d) else "工作日"})' for d in + history_date_list]) + # 处理目标日期 + show_date = f'{datetime.date.strftime(show_date_obj, "%Y-%m-%d")}({"节假日" if is_holiday(show_date_obj) else "工作日"})' + show_date_is_holiday = is_holiday(show_date_obj) + # 处理模板日期 + template_date = f'{datetime.date.strftime(template_date_obj, "%Y-%m-%d")}({"节假日" if is_holiday(template_date_obj) else "工作日"})' + template_date_is_holiday = is_holiday(template_date_obj) + return history_date, show_date, show_date_is_holiday, template_date, template_date_is_holiday + + +if __name__ == '__main__': + get_date_type("2026-06-13", "2026-06-01") diff --git a/ai/utils/show_database.py b/ai/utils/show_database.py new file mode 100644 index 0000000..c7067b6 --- /dev/null +++ b/ai/utils/show_database.py @@ -0,0 +1,136 @@ +import pymysql +from pymysql.cursors import DictCursor +from ai.utils.sql import * +import datetime +import pandas as pd + + +class GetData: + def __init__(self, db_config): + self.conn = pymysql.connect(**db_config) + self.cur = self.conn.cursor(cursor=DictCursor) + + def exit(self): + try: + self.cur.close() + self.conn.close() + except Exception as e: + print(e) + + # 获取指定日期范围的排片数据 + def get_show_data(self, start_datetime, end_datetime): + # print(self.cur.mogrify(GET_SHOW_DATA, (start_datetime, end_datetime))) + self.cur.execute(GET_SHOW_DATA, (start_datetime, end_datetime)) + show_data = self.cur.fetchall() + show_data_mapping = { + 'hall_name': '影厅别名', + 'hall_id': '影厅id', + 'movie_name': '影片别名', + 'movie_id': '本地影片id', + 'language': '语言', + 'show_date': '放映日期', + 'start_time': '开始时间', + 'end_time': '结束时间', + 'length': '片长', + 'duration': '场间', + } + return self.format_to_csv(show_data, show_data_mapping) + + # 获取指定范围的影片销售数据 + def get_sell_data(self, start_datetime, end_datetime): + # print(self.cur.mogrify(GET_SELL_DATA, (start_datetime, end_datetime))) + self.cur.execute(GET_SELL_DATA, (start_datetime, end_datetime, start_datetime, end_datetime)) + sell_data = self.cur.fetchall() + sell_data_mapping = { + 'movie_name': '影片别名', + 'movie_id': '本地影片id', + 'show_date': '放映日期', + 'start_time': '开始时间', + 'hall_name': '影厅别名', + 'hall_id': '影厅id', + 'total_count': '场次观影人数', + 'total_income': '场次收入', + 'seat_num': '影厅座位数', + 'average_price': '平均价', + 'rate': '上座率(单位%)', + } + return self.format_to_csv(sell_data, sell_data_mapping) + + # 获取指定范围的影片销售数据 + def get_total_income(self, start_datetime, end_datetime): + # print(self.cur.mogrify(GET_SELL_DATA, (start_datetime, end_datetime))) + self.cur.execute(GET_TOTAL_INCOME, (start_datetime, end_datetime, start_datetime, end_datetime)) + return self.cur.fetchone()['total_income'] + + # 获取指定日期的可放映影片 + def get_available_movie(self, date): + start_datetime = datetime.datetime.strptime(date, '%Y-%m-%d') + datetime.timedelta(hours=6) + end_datetime = datetime.datetime.strptime(date, '%Y-%m-%d') + datetime.timedelta(hours=29, minutes=59, + seconds=59) + self.cur.execute(GET_AVAILABLE_MOVIE, (start_datetime, end_datetime)) + available_movie = self.cur.fetchall() + available_movie_list = [ + f"\t《{m['cinema_movie_alias']}》 - 影片id:{m['cinema_movie_id']},影片时长:{m['cinema_movie_time']},语言:{m['language']},制式:{m['media_type']}, 最早排片开始时间:{m['cinema_movie_start_datetime']}, 最晚排片截止时间:{m['cinema_movie_end_datetime']};" + for m in available_movie] + return '\n'.join(available_movie_list) + + # 获取新上映的影片 + def get_new_movie(self, date): + start_datetime = datetime.datetime.strptime(date, '%Y-%m-%d') + end_datetime = datetime.datetime.strptime(date, '%Y-%m-%d') + datetime.timedelta(hours=29, minutes=59, + seconds=59) + self.cur.execute(GET_NEW_MOVIE, (start_datetime, end_datetime)) + new = self.cur.fetchall() + new_list = [f"《{m['cinema_movie_alias']}》" for m in new] + return new_list + + # 获取影厅支持的制式 + def get_hall_media_type(self): + self.cur.execute(GET_HALL_MEDIA_TYPE) + hall_data = self.cur.fetchall() + # print(hall_data) + hall_dict = dict() + + # 处理成对应的字典格式 {"影厅别名": {"hall_id":影厅id,"media_type": 影片制式列表}} + for hall in hall_data: + if hall['cinema_hall_name'] not in hall_dict.keys(): + hall_dict[hall['cinema_hall_name']] = { + 'hall_id': hall['cinema_hall_id'], + 'media_type': [hall['media_type'], ] + } + else: + hall_dict[hall['cinema_hall_name']]['media_type'].append(hall['media_type']) + # 拼装字符串 + hall_str_list = [] + for hall, val in hall_dict.items(): + hall_str_list.append(f"\t{hall} - 影厅id:{val['hall_id']},支持制式:{','.join(val['media_type'])};") + return '\n'.join(hall_str_list) + + # 获取模板日期 + def get_template_date(self, start_datetime, end_datetime): + basic_val = 4 + self.cur.execute(GET_TEMPLATE_DATE, (start_datetime, end_datetime, basic_val)) + template_date = self.cur.fetchall()[0]['template_date'] + return template_date + + # 数据转csv格式字符串 + @staticmethod + def format_to_csv(dict_data, mapping): + # print(dict_data) + new_dict = [{mapping[k]: v for k, v in d.items()} for d in dict_data] + return pd.DataFrame(new_dict).to_csv(index=False) + + def get_media_type(self): + self.cur.execute(GET_HALL_MEDIA_TYPE) + + +if __name__ == '__main__': + from main import cinema_list + + data = GetData(cinema_list[0]['db']) + # print(data.get_show_data('2026-05-28 06:00:00', '2026-06-04 05:59:59')) + # print(data.get_sell_data('2026-05-28 06:00:00', '2026-06-04 05:59:59')) + # print(data.get_available_movie('2026-06-05')) + print(data.get_new_movie('2026-06-05')) + # print(data.get_hall_media_type()) + # print(data.get_template_date('2026-05-28 06:00:00', '2026-06-04 05:59:59')) diff --git a/ai/utils/show_process.py b/ai/utils/show_process.py new file mode 100644 index 0000000..99d5844 --- /dev/null +++ b/ai/utils/show_process.py @@ -0,0 +1,54 @@ +from ai.models import * +from ai.utils.show_prompt import * +import datetime +from django_redis import get_redis_connection + +# ai排片主流程 +def show_main_process(): + # 查看状态 + redis_conn = get_redis_connection() + redis_key = f'ai_show{datetime.date.today().strftime("%Y%m%d")}' + if redis_conn.exists(redis_key): + return False + # 获取影院列表 + redis_conn.set(redis_key, 1, ex=60*60*20) + test_cinema_list = TestCinema.objects.filter(is_active=True).all() + # 生成目标日期 + show_date = datetime.date.strftime(datetime.date.today() + datetime.timedelta(days=3), '%Y-%m-%d') + for cinema in test_cinema_list: + print(cinema.name) + show_ai = ShowAI(cinema, show_date) + prompt = show_ai.general_prompt() + start = datetime.datetime.now() + result, message, tokens = show_ai.get_show_result_ai() + end = datetime.datetime.now() + print('prompt:', prompt) + print('result:', result) + print('message:', message) + print('tokens:', tokens) + # 获取排片数据 + _show = next((s for s in result.split('------') if s.startswith('\n影厅别名,影厅id')), '') + _show = _show.strip() + print('_show:', _show) + # 获取销售额 + _sales = next((s for s in result.split('\n') if s.startswith('预估销售数据')), '').replace('预估销售数据:', '') + _sales = _sales.replace('约', '').replace('元', '') + print('_sales:', _sales) + # 处理返回结果 + try: + AiShow.objects.create( + cinema=cinema.name, + zz_code=cinema.zz_code, + show_date=show_date, + is_ai_show=True, + show=_show, + sales=_sales, + prompt=prompt, + result=result, + message=message, + take_times=int((end - start).seconds), + take_tokens=tokens + ) + except Exception as e: + print(e) + return True diff --git a/ai/utils/show_prompt.py b/ai/utils/show_prompt.py new file mode 100644 index 0000000..cd84653 --- /dev/null +++ b/ai/utils/show_prompt.py @@ -0,0 +1,130 @@ +from django.db.models import Q +from ai.models import PromptTemplate +from ai.utils.show_database import GetData +from ai.utils.datetime_format import (get_data_datetime, get_date_type) +import datetime +import random +import json +from openai import OpenAI + + +class ShowAI: + def __init__(self, cinema_info, show_date): + self.cinema_name = cinema_info.name + self.cinema_zz_code = cinema_info.zz_code + self.cinema_db_config = json.loads(cinema_info.db_config) + self.user_config = json.loads(cinema_info.user_config) + self.show_date = show_date + self.prompt = "" + self.client = OpenAI( + api_key='sk-b7993bf4c8844e79bf84f08f07ff86d9', + base_url='https://api.deepseek.com', + ) + + # 生成提示词 + def general_prompt(self): + data = GetData(self.cinema_db_config) + # 准备各种日期时间 + history_start, history_end, target_start, target_end = get_data_datetime(self.show_date) + temp = datetime.datetime.strftime(data.get_template_date(history_start, history_end), '%Y-%m-%d') + history_date, target_date, target_date_is_holiday, template_date, template_date_is_holiday = get_date_type( + self.show_date, temp) + # print(history_start, history_end, target_start, target_end, history_date, show_date, template_date) + + # 开始处理提示词 + prompt_list = [] + # 处理排片数据 + pre_data = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='PreData')).first().prompt_val # 获取提示词模板 + pre_data = pre_data.replace('{history_show}', data.get_show_data(history_start, history_end)) # 处理排片数据 + pre_data = pre_data.replace('{history_sales}', data.get_sell_data(history_start, history_end)) # 处理销售数据 + pre_data = pre_data.replace('{target_already_show}', + data.get_show_data(target_start, target_end)) # 处理目标日期已排场次数据 + pre_data = pre_data.replace('{reference_date}', history_date) # 处理参考日期 + pre_data = pre_data.replace('{template_date}', template_date) # 处理模板日期 + pre_data = pre_data.replace('{target_date}', target_date) # 处理目标日期 + prompt_list.append(pre_data) + + # 处理角色说明 + role = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='Role')).first().prompt_val # 获取提示词模板 + role = role.replace('{template_date}', template_date) + prompt_list.append(role) + + # 处理影片和影厅信息 + movie_hall = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='MovieHall')).first().prompt_val # 获取提示词模板 + movie_hall = movie_hall.replace('{available_movie}', data.get_available_movie(self.show_date)) # 处理可排影片 + movie_hall = movie_hall.replace('{hall_info}', data.get_hall_media_type()) # 处理影厅信息 + free_hall = self.user_config['holiday_date_free_hall'] if target_date_is_holiday else self.user_config[ + 'holiday_date_free_hall'] + movie_hall = movie_hall.replace('{free_hall}', + '\t无' if len(free_hall) == 0 else '\n'.join( + f'\t{h};' for h in free_hall)) # 处理不可排片影厅 + prompt_list.append(movie_hall) + + # 处理具体要求 + rules = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='Rules')).first().prompt_val # 获取提示词模板 + rules = rules.replace('{template_date}', template_date) # 处理模板日期 + # 获取新片 + new_movie_str_list = [] + new_movie = data.get_new_movie(self.show_date) + for new in new_movie: + new_movie_str_list.append( + f"\t增加{new}的场次,建议排{str(random.randint(2, 4))}场,可根据影片热度和口碑适当增加;") + rules = rules.replace('{new_movie}', + '\t无新片上映' if len(new_movie_str_list) == 0 else '\n'.join(new_movie_str_list)) # 处理新片数据 + prompt_list.append(rules) + + # 处理基本规则 + basic = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='Basic')).first().prompt_val # 获取提示词模板 + start = self.user_config['holiday_date_start'] if target_date_is_holiday else self.user_config[ + 'work_date_start'] + end = self.user_config['holiday_date_end'] if target_date_is_holiday else self.user_config['work_date_end'] + basic = basic.replace('{start}', start) + basic = basic.replace('{end}', end) + prompt_list.append(basic) + + # 处理增减场次规则 + add_reduce = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='AddReduce')).first().prompt_val # 获取提示词模板 + prompt_list.append(add_reduce) + + # 处理黄金时段 + prime_time = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='PrimeTime')).first().prompt_val # 获取提示词模板 + prompt_list.append(prime_time) + + # 处理输出目标 + output = PromptTemplate.objects.filter( + Q(del_flag=False) & Q(prompt_type='show') & Q(prompt_key='Output')).first().prompt_val # 获取提示词模板 + prompt_list.append(output) + + # 打印提示词 + self.prompt = '\n'.join(prompt_list) + print(self.prompt) + # 关闭数据库连接 + data.exit() + return self.prompt + + # 请求AI + def get_show_result_ai(self): + # 生成提示词 + # self.general_prompt() + # 与DeepSeek开始对话 + print("与DeepSeek开始对话") + response = self.client.chat.completions.create( + model='deepseek-v4-pro', + messages=[ + {'role': 'system', 'content': 'You are a helpful assistant'}, + {'role': 'user', 'content': self.prompt}, + ], + stream=False, + reasoning_effort="high", + extra_body={"thinking": {"type": "enabled"}} + ) + # print(dict(response)) + # print(response.choices[0].message.content) + return response.choices[0].message.content, response.model_dump_json(), response.usage.model_dump_json() diff --git a/ai/utils/sql.py b/ai/utils/sql.py new file mode 100644 index 0000000..05d2f31 --- /dev/null +++ b/ai/utils/sql.py @@ -0,0 +1,182 @@ +# 排片数据 +GET_SHOW_DATA = """ +SELECT hi.cinema_hall_name as hall_name, + hi.cinema_hall_id as hall_id, + mi.cinema_movie_alias as movie_name, + mi.cinema_movie_id as movie_id, + mlt.cinema_movie_lang_type_desc as language, + DATE_FORMAT(ms.cinema_movie_show_start_time, '%%Y-%%m-%%d') as show_date, + DATE_FORMAT(ms.cinema_movie_show_start_time, '%%H:%%i') as start_time, + DATE_FORMAT(ms.cinema_movie_show_end_time, '%%H:%%i') as end_time, + CAST(TIME_TO_SEC(TIMEDIFF(ms.cinema_movie_show_end_time, ms.cinema_movie_show_start_time)) / + 60 AS UNSIGNED) as length, + IFNULL(MINUTE(TIMEDIFF(ms.cinema_movie_show_start_time, (SELECT MAX(cinema_movie_show_end_time) + FROM cinema_movie_show t + WHERE t.cinema_hall_id = ms.cinema_hall_id + AND t.cinema_movie_show_start_date = ms.cinema_movie_show_start_date + AND t.cinema_del_flag = 1 + AND ms.cinema_movie_show_start_time >= t.cinema_movie_show_end_time))), + 0) as duration +FROM cinema_movie_show ms + LEFT JOIN cinema_hall_info hi ON ms.cinema_hall_id = hi.cinema_hall_id + LEFT JOIN cinema_movie_info mi ON ms.cinema_movie_id = mi.cinema_movie_id + LEFT JOIN cinema_movie_lang_type mlt ON mi.cinema_movie_lang_id = mlt.cinema_movie_lang_type_id +WHERE ms.cinema_del_flag = 1 + AND ms.cinema_movie_show_start_time >= %s + AND ms.cinema_movie_show_start_time <= %s +ORDER BY show_date, hall_id, start_time; +""" + +# 获取销售数据 +GET_SELL_DATA = """ +SELECT md.movie_name, + md.movie_id, + md.show_date, + md.start_time, + md.hall_name, + md.hall_id, + SUM(md.count) as total_count, + SUM(md.income) as total_income, + md.seat_num, + ROUND(IF(SUM(md.income) = 0, 0.0, SUM(md.income) / SUM(md.count)), 2) as average_price, + ROUND(SUM(md.count) / md.seat_num * 100, 2) as rate +FROM ( + -- 售票 + SELECT ms.cinema_movie_show_id as cinema_movie_show_id, + mi.cinema_movie_alias as movie_name, + mi.cinema_movie_id as movie_id, + DATE_FORMAT(ms.cinema_movie_show_start_time, '%%Y-%%m-%%d') as show_date, + CAST(TIME(ms.cinema_movie_show_start_time) AS CHAR) as start_time, + hi.cinema_hall_name as hall_name, + hi.cinema_hall_id as hall_id, + SUM(IF(sl.cinema_ticket_status <> 3, 1, 0)) as count, + SUM(IF(sl.cinema_ticket_status <> 3, sl.cinema_ticket_income, 0)) as income, + ms.cinema_hall_seat_num as seat_num + FROM cinema_movie_show ms + LEFT JOIN cinema_sell_log sl ON ms.cinema_movie_show_id = sl.cinema_movie_show_id + JOIN cinema_movie_info mi ON ms.cinema_movie_id = mi.cinema_movie_id + JOIN cinema_hall_info hi ON ms.cinema_hall_id = hi.cinema_hall_id + WHERE ms.cinema_movie_show_joinflg = 0 + AND ms.cinema_movie_show_start_time >= %s + AND ms.cinema_movie_show_start_time <= %s + AND ms.cinema_del_flag = 1 + AND NOT (ms.cinema_movie_show_sold_num = 0 AND ms.cinema_movie_show_status <> 1) + GROUP BY ms.cinema_movie_show_id, CAST(ms.cinema_movie_show_id AS CHAR) + UNION +-- 补登 + SELECT ms.cinema_movie_show_id as cinema_movie_show_id, + mi.cinema_movie_alias as movie_name, + mi.cinema_movie_id as movie_id, + DATE_FORMAT(ms.cinema_movie_show_start_time, '%%Y-%%m-%%d') as show_date, + CAST(TIME(ms.cinema_movie_show_start_time) AS CHAR) as start_time, + hi.cinema_hall_name as hall_name, + hi.cinema_hall_id as hall_id, + SUM(sa.cinema_sell_add_num) as count, + SUM(sa.cinema_sell_add_total) as income, + ms.cinema_hall_seat_num as seat_num + FROM cinema_sell_add sa + LEFT JOIN cinema_movie_show ms ON sa.cinema_sell_add_showid = ms.cinema_movie_show_id + LEFT JOIN cinema_hall_info hall ON ms.cinema_hall_id = hall.cinema_hall_id + LEFT JOIN cinema_show_policy_map spm ON sa.cinema_sell_add_showid = spm.cinema_movie_show_id AND + sa.cinema_sell_add_policy = spm.cinema_show_policy_map_id + JOIN cinema_hall_info hi ON ms.cinema_hall_id = hi.cinema_hall_id + JOIN cinema_movie_info mi ON ms.cinema_movie_id = mi.cinema_movie_id + WHERE ms.cinema_movie_show_joinflg = 0 + AND ms.cinema_movie_show_start_time >= %s + AND ms.cinema_movie_show_start_time <= %s + AND ms.cinema_del_flag = 1 + AND NOT (ms.cinema_movie_show_sold_num = 0 AND ms.cinema_movie_show_status <> 1) + GROUP BY ms.cinema_movie_show_id, CAST(ms.cinema_movie_show_id AS CHAR)) as md +GROUP BY md.cinema_movie_show_id, CAST(md.cinema_movie_show_id AS CHAR) +ORDER BY md.show_date, md.hall_id, md.start_time; +""" + +# 获取指定日期的可放映影片 +GET_AVAILABLE_MOVIE = """ +SELECT cmi.cinema_movie_alias, + cmi.cinema_movie_id, + cmi.cinema_movie_time, + cmlt.cinema_movie_lang_type_desc as language, + scvm.system_const_val_desc as media_type, + cmi.cinema_movie_start_datetime, + cmi.cinema_movie_end_datetime +FROM cinema_movie_info cmi + JOIN cinema_movie_lang_type cmlt ON cmi.cinema_movie_lang_id = cmlt.cinema_movie_lang_type_id + JOIN system_const_val_map scvm ON cmi.cinema_movie_media_type = scvm.system_const_val_value +WHERE scvm.system_const_sub_module = 'new_media_type' + AND cmi.cinema_movie_start_datetime <= %s + AND cmi.cinema_movie_end_datetime > %s; +""" + +# 获取新上映的影片 +GET_NEW_MOVIE = """SELECT cmi.cinema_movie_alias, + cmi.cinema_movie_id, + cmi.cinema_movie_time, + cmlt.cinema_movie_lang_type_desc as language, + scvm.system_const_val_desc as media_type, + cmi.cinema_movie_start_datetime, + cmi.cinema_movie_end_datetime +FROM cinema_movie_info cmi + JOIN cinema_movie_lang_type cmlt ON cmi.cinema_movie_lang_id = cmlt.cinema_movie_lang_type_id + JOIN system_const_val_map scvm ON cmi.cinema_movie_media_type = scvm.system_const_val_value +WHERE scvm.system_const_sub_module = 'new_media_type' + AND cmi.cinema_movie_start_datetime >= %s + AND cmi.cinema_movie_start_datetime <= %s;""" + +# 获取影厅支持的制式 +GET_HALL_MEDIA_TYPE = """ +SELECT chi.cinema_hall_name, cms.cinema_hall_id, scvm.system_const_val_desc as media_type +FROM cinema_movie_show cms + JOIN cinema_movie_info cmi ON cms.cinema_movie_id = cmi.cinema_movie_id + JOIN cinema_hall_info chi ON cms.cinema_hall_id = chi.cinema_hall_id +JOIN system_const_val_map scvm ON cmi.cinema_movie_media_type = scvm.system_const_val_value +WHERE scvm.system_const_sub_module = 'new_media_type' + AND chi.cinema_delete_flag = 0 +GROUP BY chi.cinema_hall_id, media_type; +""" + +# 获取模板日期 +GET_TEMPLATE_DATE = """ +SELECT cms.cinema_movie_show_start_date as template_date, COUNT(cinema_movie_show_id) as show_count +FROM cinema_movie_show cms +WHERE cms.cinema_del_flag = 1 + AND cms.cinema_movie_show_start_time >= %s + AND cms.cinema_movie_show_start_time <= %s +GROUP BY cms.cinema_movie_show_start_date +HAVING show_count > (SELECT COUNT(chi.cinema_hall_id) FROM cinema_hall_info chi WHERE chi.cinema_delete_flag = 0) * %s +ORDER BY template_date DESC +LIMIT 1; +""" + + +# 获取目标日期的销售总额 +GET_TOTAL_INCOME = """ +SELECT SUM(md.income) as total_income +FROM ( + -- 售票 + SELECT SUM(IF(sl.cinema_ticket_status <> 3, sl.cinema_ticket_income, 0)) as income + FROM cinema_movie_show ms + LEFT JOIN cinema_sell_log sl ON ms.cinema_movie_show_id = sl.cinema_movie_show_id + JOIN cinema_movie_info mi ON ms.cinema_movie_id = mi.cinema_movie_id + JOIN cinema_hall_info hi ON ms.cinema_hall_id = hi.cinema_hall_id + WHERE ms.cinema_movie_show_joinflg = 0 + AND ms.cinema_movie_show_start_time >= %s + AND ms.cinema_movie_show_start_time <= %s + AND ms.cinema_del_flag = 1 + AND NOT (ms.cinema_movie_show_sold_num = 0 AND ms.cinema_movie_show_status <> 1) + UNION +-- 补登 + SELECT SUM(sa.cinema_sell_add_total) as income + FROM cinema_sell_add sa + LEFT JOIN cinema_movie_show ms ON sa.cinema_sell_add_showid = ms.cinema_movie_show_id + LEFT JOIN cinema_hall_info hall ON ms.cinema_hall_id = hall.cinema_hall_id + LEFT JOIN cinema_show_policy_map spm ON sa.cinema_sell_add_showid = spm.cinema_movie_show_id AND + sa.cinema_sell_add_policy = spm.cinema_show_policy_map_id + JOIN cinema_hall_info hi ON ms.cinema_hall_id = hi.cinema_hall_id + JOIN cinema_movie_info mi ON ms.cinema_movie_id = mi.cinema_movie_id + WHERE ms.cinema_movie_show_joinflg = 0 + AND ms.cinema_movie_show_start_time >= %s + AND ms.cinema_movie_show_start_time <= %s + AND ms.cinema_del_flag = 1 + AND NOT (ms.cinema_movie_show_sold_num = 0 AND ms.cinema_movie_show_status <> 1)) md +""" \ No newline at end of file diff --git a/ai/views.py b/ai/views.py new file mode 100644 index 0000000..7497c92 --- /dev/null +++ b/ai/views.py @@ -0,0 +1,75 @@ +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from ai.models import * +import datetime +import json +from ai.utils.show_database import GetData +from ai.utils.show_process import show_main_process +from django_redis import get_redis_connection + + +# Create your views here. + + +# 手动触发ai排片 +@csrf_exempt +def manual_general_show(request): + result = show_main_process() + result_dict = { + 'status': 'success' if result else 'fail', + 'message': '生成成功' if result else '生成失败', + } + return JsonResponse(result_dict, json_dumps_params={'ensure_ascii': False}) + + +# 更新指定日期的排片 +@csrf_exempt +def get_cinema_show_result(request): + zz_code = request.GET.dict().get('cinema_code') + show_date = request.GET.dict().get('show_date') + print(zz_code, show_date) + cinema = TestCinema.objects.filter(zz_code=zz_code).first() + start = datetime.datetime.strftime(datetime.datetime.strptime(show_date, '%Y-%m-%d') + datetime.timedelta(hours=6), + '%Y-%m-%d %H:%M:%S') + end = datetime.datetime.strftime( + datetime.datetime.strptime(show_date, '%Y-%m-%d') + datetime.timedelta(hours=29, minutes=59, seconds=59), + '%Y-%m-%d %H:%M:%S') + if cinema: + data = GetData(json.loads(cinema.db_config)) + show = data.get_show_data(start, end) + income = data.get_total_income(start, end) + try: + AiShow.objects.create( + cinema=cinema.name, + zz_code=cinema.zz_code, + show_date=show_date, + is_ai_show=False, + show=show, + sales=income, + prompt='', + result='', + message='', + take_time=0, + take_tokens='0' + ) + except Exception as e: + print(e) + result_dict = { + 'status': 'success', + 'message': '生成成功', + } + return JsonResponse(result_dict, json_dumps_params={'ensure_ascii': False}) + + +# 清除redis锁 +@csrf_exempt +def clear_lock(request): + redis_conn = get_redis_connection() + redis_key = f'ai_show{datetime.date.today().strftime("%Y%m%d")}' + if redis_conn.exists(redis_key): + redis_conn.delete(redis_key) + result_dict = { + 'status': 'success', + 'message': '完成', + } + return JsonResponse(result_dict, json_dumps_params={'ensure_ascii': False}) diff --git a/dingxin_toolbox_drf/settings.py b/dingxin_toolbox_drf/settings.py index 8b5d511..659c951 100644 --- a/dingxin_toolbox_drf/settings.py +++ b/dingxin_toolbox_drf/settings.py @@ -146,6 +146,8 @@ INSTALLED_APPS = [ 'dspt_api', 'product', 'config', + 'group', + 'ai', 'django_celery_beat', # 定时任务 'django_celery_results', # 定时任务 ] diff --git a/dingxin_toolbox_drf/urls.py b/dingxin_toolbox_drf/urls.py index 8898acf..98194f0 100644 --- a/dingxin_toolbox_drf/urls.py +++ b/dingxin_toolbox_drf/urls.py @@ -33,6 +33,7 @@ urlpatterns = [ path('docs/', include_docs_urls(title='接口文档')), path('prd/', include('product.urls')), path('config/', include('config.urls')), + path('ai/', include('ai.urls')), ] websocket_urlpatterns = [ diff --git a/reqirement_linux.txt b/reqirement_linux.txt index 28f094a4bfd3c01087077e74f5a143d8eedff2f7..11cad1c5479860917ba5e689964f0e98b55902b1 100644 GIT binary patch literal 1942 zcmZ8iO>g5i5WVZah!ADTNe((JTA;vU)22u-JsC7bTZ}1^A*tB%ukRbu&Te{~502iv zc{398yB~(0wXCUxwJwRI#yYKD#bIdJLw2v)4MWE^WW=Km>M#r ElEm37jya!aC| zhHWa5Qo!qWQ>re>?ZsrXV~aQ6;Ftxn+m`VLHTN>CKELJ&@3k>ngpsNdDyfVLD`DKQ z?alfLu-)dAkn};LVuOzYS6j&g*_`nlpU|2zz(fF1@@CgTjXHbthb9rCO0{)i7DiuH zpf1q_^0ie_?W8cujskuZ=2XaNc^ZaC#@RzL>QRrYTV^HDFvz$k)!r;eZSgTYW&D;L zlsBJg&bT0EL3T%-IvbO)$)I<11*SUqf)bt@MdeL4qnuQEloNup^{Ao@+9#KRcC4>5 z!R{D@pHQs>bl@FfS#{?GM5KT*bM^$xzrP84%tLP}Gh20;M7>vecscJLaUs@|5~HHQ z7p2@n>XMX=8qE-!zMElSEKa*{3OJyv#LI5 zWuPEP0bCa(y8?d~R9s9##g>saN)XO;0Q>6f0tO1YeyQmlG*<6_EjqviCgM75ZO$Xd!8v2$AHc8IOJV^`R3S+|AvWqn>>|NcQ; zQYSC#pW-*L$r&-^*T2x{>np--IXHfeGe%G3K4mC0_*!CFlYpPD3_Z)<5f^w#nr6cu ziItn%L1iC>R)U$XNT74KVcx~Wf#Kh8&-f%njZb`TQZe2a2)s~Bc+u~Nma5hxWPs6U zpgEwSA@oF>5$9Rte*=|>R^y`P`vAFC@!e$F%CYJyTy#?gi-+lboJr?f(&ayA92SLL zEH@0%i=aAWBAjJcf3}1F1K^{tZPW9|Tl7n3P*T(O%@gk_3h~sqs$NN2w}Pg<1$MtK zW>>_UuAmotP?)Qh0BO(OfN}y%fR=#Y&whgx0C}hu@RWBFsA#o6@FyVg6Io}G2oLx%} z2m%tjr@O1Vx@Z3VXPzF?$8<>F>hn6iO0B*Z>Fe}Wnx}pGJ8e@Jwk!R9IkFwoTDCr| z(uc5P`$pe3pFQgAJ}pv{2Khb6uS+NWO&(%h=zE!c2mRisW7yirGOmJCUAj{}8%5;2 zeXh^h-bLh2S>`!I8#rc>#c1wSORt&^qZ(%C2zRP+r_WXq`m~HJR9Wj-2*FNYVic~x zd>t6ZRuGeCl@Woom0#f%-(EgjF}KZlE;1%FXbu;$7Y{0(O=4`KKl`Z4%oSFRcOkau zVDV^_b0uWkT-E33@TLbVRo3WKr~lv+PCiA&pawDDQX$qwx(^g@qDEAJdKPgKTN7EA z;sgbt1nMH1HBfme(HgP0lP?O}=X%WmEPNgvT=>5fvD5D$LFMza2iQyBy2vm@O@qAM zT`SD`xyl)dL+uAuMJ}{qK3tLQM9}=Pw#4)dbFqt4X3hOFXH(Cq>e+DS z<$3)_#XAYDD=xe6ZS-LpvwAkFo9bco|4x)0_$lzM#1RN!*}KMbU(`L$c01|Dtr$f& zr$ApeCTf0(Tl)^mS=WyTjAJiN=l915BRea*dK7m zUh-ypext#~_@0?!_%U)b)$KWlJy8C=_<0bIr?8=1c=G(b*O^`(^x#H+e^4b{RMo~L;fH@l9(^TjF;JpZZz7@-!p?b$dY47#DNx!9^A|DKx z1vtk3v+DUScMuEJ)cC(BK3%@i#}V2*k7KW6bP7B~C>yw9Dr#o~`*uM!o)Pd&F;#Ew z3-&JWE4C>ah|WD;I#g$gTf7VZefsN+*7E^c_!{(5aFp_%A@@P&*cp|zKhirfXT;uI zrAH{gzERB9*!EG?(yu|^Qt#!6bKWn#>SMmyvP_xh$kXC8%yaG2QC?4HMB`T$%yW;| z*Kx&*%6P%C$9~Cs)l)C7_QuS`42J(A9dOJP}2QP_Ep7;8@=yeue?$J|p h9~GeZg?x%eW}}(q#lV($6;;Cz_bXf1Te;pl{{f^h0}cQH diff --git a/update/migrations/0034_cinema_url.py b/update/migrations/0034_cinema_url.py new file mode 100644 index 0000000..2b80a0b --- /dev/null +++ b/update/migrations/0034_cinema_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2026-05-15 03:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('update', '0033_alter_clientrelease_md5'), + ] + + operations = [ + migrations.AddField( + model_name='cinema', + name='url', + field=models.CharField(default=models.CharField(help_text='影院ip', max_length=20, verbose_name='影院ip'), help_text='影院url', max_length=50, verbose_name='影院url'), + ), + ]