From d9ba319269e3666ebb6c658822c1d1d8a8b14f92 Mon Sep 17 00:00:00 2001 From: sophon Date: Sun, 10 Aug 2025 19:14:15 +0800 Subject: [PATCH] modify scripts --- src/config/config.py | 3 + src/db_utils/reports_mysql.py | 195 ++++++++++++++ src/em_reports/fetch.py | 18 +- .../33c9c4443fa8_auto_update_from_stockdb.py | 253 ++++++++++++++++++ .../c72bb1f27166_auto_update_from_stockdb.py | 62 +++++ src/sqlalchemy/models/stockdb.py | 225 ++++++++++++++++ 6 files changed, 754 insertions(+), 2 deletions(-) create mode 100644 src/db_utils/reports_mysql.py create mode 100644 src/sqlalchemy/migrations/stockdb/versions/33c9c4443fa8_auto_update_from_stockdb.py create mode 100644 src/sqlalchemy/migrations/stockdb/versions/c72bb1f27166_auto_update_from_stockdb.py diff --git a/src/config/config.py b/src/config/config.py index 17be45a..5bcc876 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -8,6 +8,9 @@ db_config = { 'password': 'mysqlpw', 'database': 'stockdb' } +# 读取环境变量,来修改 db_config 中host的值 +if 'DB_HOST' in os.environ: + db_config['host'] = os.environ['DB_HOST'] home_dir = os.path.expanduser("~") global_host_data_dir = f'{home_dir}/hostdir/stock_data' diff --git a/src/db_utils/reports_mysql.py b/src/db_utils/reports_mysql.py new file mode 100644 index 0000000..1da5597 --- /dev/null +++ b/src/db_utils/reports_mysql.py @@ -0,0 +1,195 @@ +import pymysql +from pymysql.cursors import DictCursor +import logging +import sys +from datetime import datetime + +class DatabaseConnectionError(Exception): + pass + +class StockReportMysql: + # 定义类属性(静态变量) + TBL_STOCK = 'reports_stock' + TBL_NEW_STOCK = 'reports_newstrock' # 注意原拼写可能存在笔误(strock应为stock) + TBL_STRATEGY = 'reports_strategy' + TBL_MACRESEARCH = 'reports_macresearch' + TBL_INDUSTRY = 'reports_industry' + + def __init__(self, db_host, db_user, db_password, db_name, port=3306): + """ + 初始化MySQL连接 + :param db_host: 数据库主机地址 + :param db_user: 数据库用户名 + :param db_password: 数据库密码 + :param db_name: 数据库名称 + :param port: 数据库端口,默认3306 + """ + self.db_host = db_host + self.db_user = db_user + self.db_password = db_password + self.db_name = db_name + self.port = port + self.conn = None + self.cursor = None + try: + self.conn = pymysql.connect( + host=self.db_host, + user=self.db_user, + password=self.db_password, + database=self.db_name, + port=self.port, + charset='utf8mb4' # 支持中文 + ) + #self.cursor = self.conn.cursor(dictionary=False) # 使用非字典游标,保持与原代码兼容 + self.cursor = self.conn.cursor() # 使用默认游标 + except pymysql.MySQLError as e: + logging.error(f"数据库连接失败: {e}") + raise DatabaseConnectionError("数据库连接失败") + + def __get_table_columns_and_defaults(self, tbl_name): + """获取表的列信息及默认值(适配MySQL)""" + try: + # 查询information_schema获取列信息 + self.cursor.execute(""" + SELECT COLUMN_NAME, COLUMN_DEFAULT + FROM information_schema.COLUMNS + WHERE TABLE_NAME = %s AND TABLE_SCHEMA = %s + """, (tbl_name, self.db_name)) + columns = self.cursor.fetchall() + column_info = {} + for col in columns: + col_name = col[0] + default_value = col[1] + # MySQL默认值可能包含函数(如CURRENT_TIMESTAMP),需要特殊处理 + column_info[col_name] = default_value + return column_info + except pymysql.MySQLError as e: + logging.error(f"获取表结构失败: {e}") + return None + + def __check_and_process_data(self, data, tbl_name): + """数据校验和处理(逻辑与原代码保持一致)""" + column_info = self.__get_table_columns_and_defaults(tbl_name=tbl_name) + if column_info is None: + return None + processed_data = {} + for col, default in column_info.items(): + if col == 'id': # 自增主键,不需要用户提供 + continue + if col == 'created_at' or col == 'updated_at': # 日期字段由数据库或代码控制 + continue + if col in ['author', 'authorID']: + # 将列表转换为逗号分隔字符串 + values = data.get(col, []) + processed_data[col] = ','.join(values) if values else None + # 确保不超过255字符 + if processed_data[col] and len(processed_data[col]) > 250: + processed_data[col] = processed_data[col][:250] + elif col in data: + processed_data[col] = data[col] + else: + # 使用默认值 + pass + return processed_data + + def insert_or_update_common(self, data, tbl_name, uniq_key='infoCode'): + """插入或更新数据(适配MySQL的ON DUPLICATE KEY UPDATE)""" + try: + processed_data = self.__check_and_process_data(data, tbl_name) + if processed_data is None: + return None + + columns = ', '.join(processed_data.keys()) + values = list(processed_data.values()) + placeholders = ', '.join(['%s' for _ in values]) # MySQL使用%s作为占位符 + + # 构造更新子句(排除唯一键字段) + update_clause = ', '.join( + [f"{col}=VALUES({col})" for col in processed_data.keys() if col != uniq_key] + ) + ', updated_at=NOW()' # MySQL使用NOW()获取当前时间 + + sql = f''' + INSERT INTO {tbl_name} ({columns}, created_at, updated_at) + VALUES ({placeholders}, NOW(), NOW()) + ON DUPLICATE KEY UPDATE {update_clause} + ''' + self.cursor.execute(sql, values) + self.conn.commit() + + # 获取插入或更新后的记录ID + self.cursor.execute(f"SELECT id FROM {tbl_name} WHERE {uniq_key} = %s", (data[uniq_key],)) + result = self.cursor.fetchone() + return result[0] if result else None + except pymysql.MySQLError as e: + logging.error(f"插入或更新数据失败: {e}") + self.conn.rollback() # 出错时回滚 + return None + + def update_pages(self, data, tbl_name, uniq_key='infoCode'): + """更新附件页数(使用参数化查询防止SQL注入)""" + try: + # 注意:原代码直接拼接SQL有注入风险,此处改为参数化查询 + sql = f''' + UPDATE {tbl_name} + SET attachPages = %s + WHERE id = %s + ''' + self.cursor.execute(sql, (data['attachPages'], data['id'])) + self.conn.commit() + return data['id'] + except pymysql.MySQLError as e: + logging.error(f"更新页数失败: {e}") + self.conn.rollback() + return None + + def query_reports_comm(self, tbl_name, querystr='', limit=None): + """查询报告列表(适配MySQL语法)""" + try: + # 验证表名合法性 + valid_tables = [ + self.TBL_STOCK, self.TBL_NEW_STOCK, + self.TBL_INDUSTRY, self.TBL_MACRESEARCH, + self.TBL_STRATEGY + ] + if tbl_name not in valid_tables: + logging.warning(f'无效的表名: {tbl_name}') + return None + + # 构造查询SQL + sql = f""" + SELECT id, infoCode, title, orgSName, industryName, stockName, publishDate + FROM {tbl_name} + WHERE 1=1 {querystr} + """ + # 添加限制条件 + if limit: + sql += f' LIMIT {limit}' + + self.cursor.execute(sql) + results = self.cursor.fetchall() + + # 获取列名 + column_names = [description[0] for description in self.cursor.description] + + # 转换为字典列表 + result_dict_list = [] + for row in results: + row_dict = {column_names[i]: value for i, value in enumerate(row)} + result_dict_list.append(row_dict) + + return result_dict_list + except pymysql.MySQLError as e: + logging.error(f"查询失败: {e}") + return None + + def __del__(self): + """析构函数,关闭数据库连接(适配pymysql)""" + try: + # pymysql中通过ping()判断连接是否有效,若连接已关闭会抛出异常 + if self.conn: + self.conn.ping() # 尝试检测连接是否存活 + self.cursor.close() + self.conn.close() + except (pymysql.MySQLError, AttributeError): + # 捕获连接已关闭、游标不存在等异常,避免析构时报错 + pass \ No newline at end of file diff --git a/src/em_reports/fetch.py b/src/em_reports/fetch.py index c06ea52..449c937 100644 --- a/src/em_reports/fetch.py +++ b/src/em_reports/fetch.py @@ -11,8 +11,9 @@ from datetime import datetime, timedelta from functools import partial import src.crawler.em.reports as em import src.utils.utils as utils -from src.config.config import global_host_data_dir, global_share_db_dir +from src.config.config import global_host_data_dir, global_share_db_dir, db_config from src.db_utils.reports import StockReportDB, DatabaseConnectionError +from src.db_utils.reports_mysql import StockReportMysql from src.logger.logger import setup_logging import PyPDF2 @@ -83,6 +84,17 @@ def fetch_reports_list_general(fetch_func, table_name, s_date, e_date, data_dir_ # 统一以 infoCode 为 UNIQE 键,所以这里对它进行赋值 if row.get('infoCode') is None and row.get('encodeUrl'): row['infoCode'] = row['encodeUrl'] + row['count_all'] = row.get('count', 0) # 兼容旧数据 + row['curr_column'] = row.get('column', 'None') # 兼容旧数据 + if 'stockName' not in row or row['stockName'] is None or row['stockName']=='': + row['stockName'] = '' + if 'stockCode' not in row or row['stockCode'] is None or row['stockCode']=='': + row['stockCode'] = '' + if 'newPeIssueA' in row and row['newPeIssueA'] is None: + try: + row['newPeIssueA'] = float(row.get('newPeIssueA', 0)) + except ValueError: + row['newPeIssueA'] = 0.0 row_id = db_tools.insert_or_update_common(row, table_name) if row_id: logging.debug(f'insert one row. rowid:{row_id}, ') @@ -325,7 +337,9 @@ def main(cmd, mode, args_debug, args_force, begin, end): # 初始化DB global db_tools try: - db_tools = StockReportDB(db_path) + #db_tools = StockReportDB(db_path) + db_tools = StockReportMysql(db_host=db_config['host'], db_user=db_config['user'], db_password=db_config['password'], db_name = db_config['database'], port=3306) # 使用配置文件中的数据库配置 + # 进行数据库操作 except DatabaseConnectionError as e: logging.error(f"数据库连接失败: {e}") diff --git a/src/sqlalchemy/migrations/stockdb/versions/33c9c4443fa8_auto_update_from_stockdb.py b/src/sqlalchemy/migrations/stockdb/versions/33c9c4443fa8_auto_update_from_stockdb.py new file mode 100644 index 0000000..9197193 --- /dev/null +++ b/src/sqlalchemy/migrations/stockdb/versions/33c9c4443fa8_auto_update_from_stockdb.py @@ -0,0 +1,253 @@ +"""Auto update from stockdb + +Revision ID: 33c9c4443fa8 +Revises: 9680e7b8e29b +Create Date: 2025-08-10 18:03:56.817265 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '33c9c4443fa8' +down_revision: Union[str, Sequence[str], None] = '9680e7b8e29b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('reports_industry', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='自增主键'), + sa.Column('infoCode', sa.String(length=255), nullable=False, comment='报告唯一标识'), + sa.Column('title', sa.String(length=512), nullable=True, comment='报告标题'), + sa.Column('stockName', sa.String(length=100), nullable=True, comment='股票名称'), + sa.Column('stockCode', sa.String(length=50), nullable=True, comment='股票代码'), + sa.Column('orgCode', sa.String(length=50), nullable=True, comment='机构代码'), + sa.Column('orgName', sa.String(length=255), nullable=True, comment='机构全称'), + sa.Column('orgSName', sa.String(length=100), nullable=True, comment='机构简称'), + sa.Column('publishDate', sa.String(length=50), nullable=True, comment='发布日期'), + sa.Column('column', sa.String(length=255), nullable=True, comment='栏目'), + sa.Column('predictNextTwoYearEps', sa.String(length=50), nullable=True, comment='未来两年EPS预测'), + sa.Column('predictNextTwoYearPe', sa.String(length=50), nullable=True, comment='未来两年PE预测'), + sa.Column('predictNextYearEps', sa.String(length=50), nullable=True, comment='明年EPS预测'), + sa.Column('predictNextYearPe', sa.String(length=50), nullable=True, comment='明年PE预测'), + sa.Column('predictThisYearEps', sa.String(length=50), nullable=True, comment='今年EPS预测'), + sa.Column('predictThisYearPe', sa.String(length=50), nullable=True, comment='今年PE预测'), + sa.Column('predictLastYearEps', sa.String(length=50), nullable=True, comment='去年EPS预测'), + sa.Column('predictLastYearPe', sa.String(length=50), nullable=True, comment='去年PE预测'), + sa.Column('actualLastTwoYearEps', sa.String(length=50), nullable=True, comment='前两年实际EPS'), + sa.Column('actualLastYearEps', sa.String(length=50), nullable=True, comment='去年实际EPS'), + sa.Column('industryCode', sa.String(length=50), nullable=True, comment='行业代码'), + sa.Column('industryName', sa.String(length=100), nullable=True, comment='行业名称'), + sa.Column('emIndustryCode', sa.String(length=50), nullable=True, comment='东方财富行业代码'), + sa.Column('indvInduCode', sa.String(length=50), nullable=True, comment='个性化行业代码'), + sa.Column('indvInduName', sa.String(length=100), nullable=True, comment='个性化行业名称'), + sa.Column('emRatingCode', sa.String(length=50), nullable=True, comment='东方财富评级代码'), + sa.Column('emRatingValue', sa.String(length=50), nullable=True, comment='东方财富评级值'), + sa.Column('emRatingName', sa.String(length=100), nullable=True, comment='东方财富评级名称'), + sa.Column('lastEmRatingCode', sa.String(length=50), nullable=True, comment='上次东方财富评级代码'), + sa.Column('lastEmRatingValue', sa.String(length=50), nullable=True, comment='上次东方财富评级值'), + sa.Column('lastEmRatingName', sa.String(length=100), nullable=True, comment='上次东方财富评级名称'), + sa.Column('ratingChange', sa.String(length=50), nullable=True, comment='评级变动'), + sa.Column('reportType', sa.Integer(), nullable=True, comment='报告类型'), + sa.Column('author', sa.String(length=255), nullable=True, comment='作者(逗号分隔)'), + sa.Column('indvIsNew', sa.String(length=20), nullable=True, comment='是否为新研报'), + sa.Column('researcher', sa.String(length=255), nullable=True, comment='研究员'), + sa.Column('newListingDate', sa.String(length=50), nullable=True, comment='新上市日期'), + sa.Column('newPurchaseDate', sa.String(length=50), nullable=True, comment='新买入日期'), + sa.Column('newIssuePrice', sa.String(length=50), nullable=True, comment='新发行价格'), + sa.Column('newPeIssueA', sa.String(length=50), nullable=True, comment='新发行市盈率A'), + sa.Column('indvAimPriceT', sa.String(length=50), nullable=True, comment='目标价上限'), + sa.Column('indvAimPriceL', sa.String(length=50), nullable=True, comment='目标价下限'), + sa.Column('attachType', sa.String(length=50), nullable=True, comment='附件类型'), + sa.Column('attachSize', sa.Integer(), nullable=True, comment='附件大小'), + sa.Column('attachPages', sa.Integer(), nullable=True, comment='附件页数'), + sa.Column('encodeUrl', sa.String(length=512), nullable=True, comment='加密URL'), + sa.Column('sRatingName', sa.String(length=100), nullable=True, comment='评级名称'), + sa.Column('sRatingCode', sa.String(length=50), nullable=True, comment='评级代码'), + sa.Column('market', sa.String(length=50), nullable=True, comment='市场'), + sa.Column('authorID', sa.String(length=255), nullable=True, comment='作者ID(逗号分隔)'), + sa.Column('count', sa.Integer(), nullable=True, comment='计数'), + sa.Column('orgType', sa.String(length=50), nullable=True, comment='机构类型'), + sa.Column('created_at', sa.DateTime(), nullable=False, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(), nullable=False, comment='更新时间'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('infoCode') + ) + op.create_table('reports_macresearch', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='自增主键'), + sa.Column('infoCode', sa.String(length=255), nullable=False, comment='报告唯一标识'), + sa.Column('json_id', sa.String(length=50), nullable=True, comment='JSON ID'), + sa.Column('title', sa.String(length=512), nullable=True, comment='报告标题'), + sa.Column('author', sa.String(length=255), nullable=True, comment='作者(逗号分隔)'), + sa.Column('orgName', sa.String(length=255), nullable=True, comment='机构全称'), + sa.Column('orgCode', sa.String(length=50), nullable=True, comment='机构代码'), + sa.Column('orgSName', sa.String(length=100), nullable=True, comment='机构简称'), + sa.Column('publishDate', sa.String(length=50), nullable=True, comment='发布日期'), + sa.Column('encodeUrl', sa.String(length=512), nullable=True, comment='加密URL'), + sa.Column('researcher', sa.String(length=255), nullable=True, comment='研究员'), + sa.Column('market', sa.String(length=50), nullable=True, comment='市场'), + sa.Column('industryCode', sa.String(length=50), nullable=True, comment='行业代码'), + sa.Column('industryName', sa.String(length=100), nullable=True, comment='行业名称'), + sa.Column('authorID', sa.String(length=255), nullable=True, comment='作者ID(逗号分隔)'), + sa.Column('count', sa.Integer(), nullable=True, comment='计数'), + sa.Column('orgType', sa.String(length=50), nullable=True, comment='机构类型'), + sa.Column('created_at', sa.DateTime(), nullable=False, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(), nullable=False, comment='更新时间'), + sa.Column('stockCode', sa.String(length=50), nullable=False, comment='股票代码(默认空字符串)'), + sa.Column('stockName', sa.String(length=100), nullable=False, comment='股票名称(默认空字符串)'), + sa.Column('attachPages', sa.Integer(), nullable=True, comment='附件页数'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('infoCode') + ) + op.create_table('reports_newstrock', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='自增主键'), + sa.Column('infoCode', sa.String(length=255), nullable=False, comment='报告唯一标识'), + sa.Column('title', sa.String(length=512), nullable=True, comment='报告标题'), + sa.Column('stockName', sa.String(length=100), nullable=True, comment='股票名称'), + sa.Column('stockCode', sa.String(length=50), nullable=True, comment='股票代码'), + sa.Column('orgCode', sa.String(length=50), nullable=True, comment='机构代码'), + sa.Column('orgName', sa.String(length=255), nullable=True, comment='机构全称'), + sa.Column('orgSName', sa.String(length=100), nullable=True, comment='机构简称'), + sa.Column('publishDate', sa.String(length=50), nullable=True, comment='发布日期'), + sa.Column('column', sa.String(length=255), nullable=True, comment='栏目'), + sa.Column('actualLastTwoYearEps', sa.String(length=50), nullable=True, comment='前两年实际EPS'), + sa.Column('actualLastYearEps', sa.String(length=50), nullable=True, comment='去年实际EPS'), + sa.Column('industryCode', sa.String(length=50), nullable=True, comment='行业代码'), + sa.Column('industryName', sa.String(length=100), nullable=True, comment='行业名称'), + sa.Column('emIndustryCode', sa.String(length=50), nullable=True, comment='东方财富行业代码'), + sa.Column('indvInduCode', sa.String(length=50), nullable=True, comment='个性化行业代码'), + sa.Column('indvInduName', sa.String(length=100), nullable=True, comment='个性化行业名称'), + sa.Column('emRatingCode', sa.String(length=50), nullable=True, comment='东方财富评级代码'), + sa.Column('emRatingValue', sa.String(length=50), nullable=True, comment='东方财富评级值'), + sa.Column('emRatingName', sa.String(length=100), nullable=True, comment='东方财富评级名称'), + sa.Column('lastEmRatingCode', sa.String(length=50), nullable=True, comment='上次东方财富评级代码'), + sa.Column('lastEmRatingValue', sa.String(length=50), nullable=True, comment='上次东方财富评级值'), + sa.Column('lastEmRatingName', sa.String(length=100), nullable=True, comment='上次东方财富评级名称'), + sa.Column('ratingChange', sa.String(length=50), nullable=True, comment='评级变动'), + sa.Column('reportType', sa.Integer(), nullable=True, comment='报告类型'), + sa.Column('author', sa.String(length=255), nullable=True, comment='作者(逗号分隔)'), + sa.Column('indvIsNew', sa.String(length=20), nullable=True, comment='是否为新研报'), + sa.Column('researcher', sa.String(length=255), nullable=True, comment='研究员'), + sa.Column('newListingDate', sa.String(length=50), nullable=True, comment='新上市日期'), + sa.Column('newPurchaseDate', sa.String(length=50), nullable=True, comment='新买入日期'), + sa.Column('newIssuePrice', sa.Float(), nullable=True, comment='新发行价格'), + sa.Column('newPeIssueA', sa.Float(), nullable=True, comment='新发行市盈率A'), + sa.Column('indvAimPriceT', sa.String(length=50), nullable=True, comment='目标价上限'), + sa.Column('indvAimPriceL', sa.String(length=50), nullable=True, comment='目标价下限'), + sa.Column('attachType', sa.String(length=50), nullable=True, comment='附件类型'), + sa.Column('attachSize', sa.Integer(), nullable=True, comment='附件大小'), + sa.Column('attachPages', sa.Integer(), nullable=True, comment='附件页数'), + sa.Column('encodeUrl', sa.String(length=512), nullable=True, comment='加密URL'), + sa.Column('sRatingName', sa.String(length=100), nullable=True, comment='评级名称'), + sa.Column('sRatingCode', sa.String(length=50), nullable=True, comment='评级代码'), + sa.Column('market', sa.String(length=50), nullable=True, comment='市场'), + sa.Column('newStockSort', sa.String(length=50), nullable=True, comment='新股分类'), + sa.Column('authorID', sa.String(length=255), nullable=True, comment='作者ID(逗号分隔)'), + sa.Column('count', sa.Integer(), nullable=True, comment='计数'), + sa.Column('orgType', sa.String(length=50), nullable=True, comment='机构类型'), + sa.Column('created_at', sa.DateTime(), nullable=False, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(), nullable=False, comment='更新时间'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('infoCode') + ) + op.create_table('reports_stock', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='自增主键'), + sa.Column('infoCode', sa.String(length=255), nullable=False, comment='报告唯一标识'), + sa.Column('title', sa.String(length=512), nullable=True, comment='报告标题'), + sa.Column('stockName', sa.String(length=100), nullable=True, comment='股票名称'), + sa.Column('stockCode', sa.String(length=50), nullable=True, comment='股票代码'), + sa.Column('orgCode', sa.String(length=50), nullable=True, comment='机构代码'), + sa.Column('orgName', sa.String(length=255), nullable=True, comment='机构全称'), + sa.Column('orgSName', sa.String(length=100), nullable=True, comment='机构简称'), + sa.Column('publishDate', sa.String(length=50), nullable=True, comment='发布日期'), + sa.Column('column', sa.String(length=255), nullable=True, comment='栏目'), + sa.Column('predictNextTwoYearEps', sa.String(length=50), nullable=True, comment='未来两年EPS预测'), + sa.Column('predictNextTwoYearPe', sa.String(length=50), nullable=True, comment='未来两年PE预测'), + sa.Column('predictNextYearEps', sa.String(length=50), nullable=True, comment='明年EPS预测'), + sa.Column('predictNextYearPe', sa.String(length=50), nullable=True, comment='明年PE预测'), + sa.Column('predictThisYearEps', sa.String(length=50), nullable=True, comment='今年EPS预测'), + sa.Column('predictThisYearPe', sa.String(length=50), nullable=True, comment='今年PE预测'), + sa.Column('predictLastYearEps', sa.String(length=50), nullable=True, comment='去年EPS预测'), + sa.Column('predictLastYearPe', sa.String(length=50), nullable=True, comment='去年PE预测'), + sa.Column('actualLastTwoYearEps', sa.String(length=50), nullable=True, comment='前两年实际EPS'), + sa.Column('actualLastYearEps', sa.String(length=50), nullable=True, comment='去年实际EPS'), + sa.Column('industryCode', sa.String(length=50), nullable=True, comment='行业代码'), + sa.Column('industryName', sa.String(length=100), nullable=True, comment='行业名称'), + sa.Column('emIndustryCode', sa.String(length=50), nullable=True, comment='东方财富行业代码'), + sa.Column('indvInduCode', sa.String(length=50), nullable=True, comment='个性化行业代码'), + sa.Column('indvInduName', sa.String(length=100), nullable=True, comment='个性化行业名称'), + sa.Column('emRatingCode', sa.String(length=50), nullable=True, comment='东方财富评级代码'), + sa.Column('emRatingValue', sa.String(length=50), nullable=True, comment='东方财富评级值'), + sa.Column('emRatingName', sa.String(length=100), nullable=True, comment='东方财富评级名称'), + sa.Column('lastEmRatingCode', sa.String(length=50), nullable=True, comment='上次东方财富评级代码'), + sa.Column('lastEmRatingValue', sa.String(length=50), nullable=True, comment='上次东方财富评级值'), + sa.Column('lastEmRatingName', sa.String(length=100), nullable=True, comment='上次东方财富评级名称'), + sa.Column('ratingChange', sa.Integer(), nullable=True, comment='评级变动'), + sa.Column('reportType', sa.Integer(), nullable=True, comment='报告类型'), + sa.Column('author', sa.String(length=255), nullable=True, comment='作者(逗号分隔)'), + sa.Column('indvIsNew', sa.String(length=20), nullable=True, comment='是否为新研报'), + sa.Column('researcher', sa.String(length=255), nullable=True, comment='研究员'), + sa.Column('newListingDate', sa.String(length=50), nullable=True, comment='新上市日期'), + sa.Column('newPurchaseDate', sa.String(length=50), nullable=True, comment='新买入日期'), + sa.Column('newIssuePrice', sa.Float(), nullable=True, comment='新发行价格'), + sa.Column('newPeIssueA', sa.Float(), nullable=True, comment='新发行市盈率A'), + sa.Column('indvAimPriceT', sa.String(length=50), nullable=True, comment='目标价上限'), + sa.Column('indvAimPriceL', sa.String(length=50), nullable=True, comment='目标价下限'), + sa.Column('attachType', sa.String(length=50), nullable=True, comment='附件类型'), + sa.Column('attachSize', sa.Integer(), nullable=True, comment='附件大小'), + sa.Column('attachPages', sa.Integer(), nullable=True, comment='附件页数'), + sa.Column('encodeUrl', sa.String(length=512), nullable=True, comment='加密URL'), + sa.Column('sRatingName', sa.String(length=100), nullable=True, comment='评级名称'), + sa.Column('sRatingCode', sa.String(length=50), nullable=True, comment='评级代码'), + sa.Column('market', sa.String(length=50), nullable=True, comment='市场'), + sa.Column('authorID', sa.String(length=255), nullable=True, comment='作者ID(逗号分隔)'), + sa.Column('count', sa.Integer(), nullable=True, comment='计数'), + sa.Column('orgType', sa.String(length=50), nullable=True, comment='机构类型'), + sa.Column('created_at', sa.DateTime(), nullable=False, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(), nullable=False, comment='更新时间'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('infoCode') + ) + op.create_table('reports_strategy', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='自增主键'), + sa.Column('infoCode', sa.String(length=255), nullable=False, comment='报告唯一标识'), + sa.Column('title', sa.String(length=512), nullable=True, comment='报告标题'), + sa.Column('author', sa.String(length=255), nullable=True, comment='作者(逗号分隔)'), + sa.Column('orgName', sa.String(length=255), nullable=True, comment='机构全称'), + sa.Column('orgCode', sa.String(length=50), nullable=True, comment='机构代码'), + sa.Column('orgSName', sa.String(length=100), nullable=True, comment='机构简称'), + sa.Column('publishDate', sa.String(length=50), nullable=True, comment='发布日期'), + sa.Column('encodeUrl', sa.String(length=512), nullable=True, comment='加密URL'), + sa.Column('researcher', sa.String(length=255), nullable=True, comment='研究员'), + sa.Column('market', sa.String(length=50), nullable=True, comment='市场'), + sa.Column('industryCode', sa.String(length=50), nullable=True, comment='行业代码'), + sa.Column('industryName', sa.String(length=100), nullable=True, comment='行业名称'), + sa.Column('authorID', sa.String(length=255), nullable=True, comment='作者ID(逗号分隔)'), + sa.Column('count', sa.Integer(), nullable=True, comment='计数'), + sa.Column('orgType', sa.String(length=50), nullable=True, comment='机构类型'), + sa.Column('created_at', sa.DateTime(), nullable=False, comment='创建时间'), + sa.Column('updated_at', sa.DateTime(), nullable=False, comment='更新时间'), + sa.Column('stockName', sa.String(length=100), nullable=False, comment='股票名称(默认空字符串)'), + sa.Column('stockCode', sa.String(length=50), nullable=False, comment='股票代码(默认空字符串)'), + sa.Column('attachPages', sa.Integer(), nullable=True, comment='附件页数'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('infoCode') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('reports_strategy') + op.drop_table('reports_stock') + op.drop_table('reports_newstrock') + op.drop_table('reports_macresearch') + op.drop_table('reports_industry') + # ### end Alembic commands ### diff --git a/src/sqlalchemy/migrations/stockdb/versions/c72bb1f27166_auto_update_from_stockdb.py b/src/sqlalchemy/migrations/stockdb/versions/c72bb1f27166_auto_update_from_stockdb.py new file mode 100644 index 0000000..d5688f4 --- /dev/null +++ b/src/sqlalchemy/migrations/stockdb/versions/c72bb1f27166_auto_update_from_stockdb.py @@ -0,0 +1,62 @@ +"""Auto update from stockdb + +Revision ID: c72bb1f27166 +Revises: 33c9c4443fa8 +Create Date: 2025-08-10 18:36:58.748680 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision: str = 'c72bb1f27166' +down_revision: Union[str, Sequence[str], None] = '33c9c4443fa8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('reports_industry', sa.Column('curr_column', sa.String(length=255), nullable=True, comment='栏目')) + op.add_column('reports_industry', sa.Column('count_all', sa.Integer(), nullable=True, comment='计数')) + op.drop_column('reports_industry', 'column') + op.drop_column('reports_industry', 'count') + op.add_column('reports_macresearch', sa.Column('count_all', sa.Integer(), nullable=True, comment='计数')) + op.drop_column('reports_macresearch', 'count') + op.add_column('reports_newstrock', sa.Column('curr_column', sa.String(length=255), nullable=True, comment='栏目')) + op.add_column('reports_newstrock', sa.Column('count_all', sa.Integer(), nullable=True, comment='计数')) + op.drop_column('reports_newstrock', 'column') + op.drop_column('reports_newstrock', 'count') + op.add_column('reports_stock', sa.Column('curr_column', sa.String(length=255), nullable=True, comment='栏目')) + op.add_column('reports_stock', sa.Column('count_all', sa.Integer(), nullable=True, comment='计数')) + op.drop_column('reports_stock', 'column') + op.drop_column('reports_stock', 'count') + op.add_column('reports_strategy', sa.Column('count_all', sa.Integer(), nullable=True, comment='计数')) + op.drop_column('reports_strategy', 'count') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('reports_strategy', sa.Column('count', mysql.INTEGER(), autoincrement=False, nullable=True, comment='计数')) + op.drop_column('reports_strategy', 'count_all') + op.add_column('reports_stock', sa.Column('count', mysql.INTEGER(), autoincrement=False, nullable=True, comment='计数')) + op.add_column('reports_stock', sa.Column('column', mysql.VARCHAR(length=255), nullable=True, comment='栏目')) + op.drop_column('reports_stock', 'count_all') + op.drop_column('reports_stock', 'curr_column') + op.add_column('reports_newstrock', sa.Column('count', mysql.INTEGER(), autoincrement=False, nullable=True, comment='计数')) + op.add_column('reports_newstrock', sa.Column('column', mysql.VARCHAR(length=255), nullable=True, comment='栏目')) + op.drop_column('reports_newstrock', 'count_all') + op.drop_column('reports_newstrock', 'curr_column') + op.add_column('reports_macresearch', sa.Column('count', mysql.INTEGER(), autoincrement=False, nullable=True, comment='计数')) + op.drop_column('reports_macresearch', 'count_all') + op.add_column('reports_industry', sa.Column('count', mysql.INTEGER(), autoincrement=False, nullable=True, comment='计数')) + op.add_column('reports_industry', sa.Column('column', mysql.VARCHAR(length=255), nullable=True, comment='栏目')) + op.drop_column('reports_industry', 'count_all') + op.drop_column('reports_industry', 'curr_column') + # ### end Alembic commands ### diff --git a/src/sqlalchemy/models/stockdb.py b/src/sqlalchemy/models/stockdb.py index 30401e6..9ec98e1 100644 --- a/src/sqlalchemy/models/stockdb.py +++ b/src/sqlalchemy/models/stockdb.py @@ -1,6 +1,7 @@ from sqlalchemy import BigInteger, Date, DateTime, Double, Float, Integer, String, text, Numeric from sqlalchemy.dialects.mysql import TINYINT, VARCHAR from typing import Optional +from sqlalchemy.sql import func from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column import datetime @@ -446,3 +447,227 @@ class FutuTradingDayModel(Base): String(20), comment="交易日类型" ) + + + +class ReportsStock(Base): + __tablename__ = "reports_stock" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="自增主键") + infoCode: Mapped[str] = mapped_column(String(255), unique=True, comment="报告唯一标识") + title: Mapped[Optional[str]] = mapped_column(String(512), comment="报告标题") + stockName: Mapped[Optional[str]] = mapped_column(String(100), comment="股票名称") + stockCode: Mapped[Optional[str]] = mapped_column(String(50), comment="股票代码") + orgCode: Mapped[Optional[str]] = mapped_column(String(50), comment="机构代码") + orgName: Mapped[Optional[str]] = mapped_column(String(255), comment="机构全称") + orgSName: Mapped[Optional[str]] = mapped_column(String(100), comment="机构简称") + publishDate: Mapped[Optional[str]] = mapped_column(String(50), comment="发布日期") + curr_column: Mapped[Optional[str]] = mapped_column(String(255), comment="栏目") # 注意:column是SQL关键字,建议后续改名 + predictNextTwoYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="未来两年EPS预测") + predictNextTwoYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="未来两年PE预测") + predictNextYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="明年EPS预测") + predictNextYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="明年PE预测") + predictThisYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="今年EPS预测") + predictThisYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="今年PE预测") + predictLastYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="去年EPS预测") + predictLastYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="去年PE预测") + actualLastTwoYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="前两年实际EPS") + actualLastYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="去年实际EPS") + industryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="行业代码") + industryName: Mapped[Optional[str]] = mapped_column(String(100), comment="行业名称") + emIndustryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富行业代码") + indvInduCode: Mapped[Optional[str]] = mapped_column(String(50), comment="个性化行业代码") + indvInduName: Mapped[Optional[str]] = mapped_column(String(100), comment="个性化行业名称") + emRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富评级代码") + emRatingValue: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富评级值") + emRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="东方财富评级名称") + lastEmRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="上次东方财富评级代码") + lastEmRatingValue: Mapped[Optional[str]] = mapped_column(String(50), comment="上次东方财富评级值") + lastEmRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="上次东方财富评级名称") + ratingChange: Mapped[Optional[int]] = mapped_column(Integer, comment="评级变动") + reportType: Mapped[Optional[int]] = mapped_column(Integer, comment="报告类型") + author: Mapped[Optional[str]] = mapped_column(String(255), comment="作者(逗号分隔)") + indvIsNew: Mapped[Optional[str]] = mapped_column(String(20), comment="是否为新研报") + researcher: Mapped[Optional[str]] = mapped_column(String(255), comment="研究员") + newListingDate: Mapped[Optional[str]] = mapped_column(String(50), comment="新上市日期") + newPurchaseDate: Mapped[Optional[str]] = mapped_column(String(50), comment="新买入日期") + newIssuePrice: Mapped[Optional[float]] = mapped_column(Float, comment="新发行价格") + newPeIssueA: Mapped[Optional[float]] = mapped_column(Float, comment="新发行市盈率A") + indvAimPriceT: Mapped[Optional[str]] = mapped_column(String(50), comment="目标价上限") + indvAimPriceL: Mapped[Optional[str]] = mapped_column(String(50), comment="目标价下限") + attachType: Mapped[Optional[str]] = mapped_column(String(50), comment="附件类型") + attachSize: Mapped[Optional[int]] = mapped_column(Integer, comment="附件大小") + attachPages: Mapped[Optional[int]] = mapped_column(Integer, comment="附件页数") + encodeUrl: Mapped[Optional[str]] = mapped_column(String(512), comment="加密URL") + sRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="评级名称") + sRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="评级代码") + market: Mapped[Optional[str]] = mapped_column(String(50), comment="市场") + authorID: Mapped[Optional[str]] = mapped_column(String(255), comment="作者ID(逗号分隔)") + count_all: Mapped[Optional[int]] = mapped_column(Integer, comment="计数") + orgType: Mapped[Optional[str]] = mapped_column(String(50), comment="机构类型") + created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), comment="创建时间") + updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间") + + +class ReportsIndustry(Base): + __tablename__ = "reports_industry" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="自增主键") + infoCode: Mapped[str] = mapped_column(String(255), unique=True, comment="报告唯一标识") + title: Mapped[Optional[str]] = mapped_column(String(512), comment="报告标题") + stockName: Mapped[Optional[str]] = mapped_column(String(100), comment="股票名称") + stockCode: Mapped[Optional[str]] = mapped_column(String(50), comment="股票代码") + orgCode: Mapped[Optional[str]] = mapped_column(String(50), comment="机构代码") + orgName: Mapped[Optional[str]] = mapped_column(String(255), comment="机构全称") + orgSName: Mapped[Optional[str]] = mapped_column(String(100), comment="机构简称") + publishDate: Mapped[Optional[str]] = mapped_column(String(50), comment="发布日期") + curr_column: Mapped[Optional[str]] = mapped_column(String(255), comment="栏目") # 注意:column是SQL关键字 + predictNextTwoYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="未来两年EPS预测") + predictNextTwoYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="未来两年PE预测") + predictNextYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="明年EPS预测") + predictNextYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="明年PE预测") + predictThisYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="今年EPS预测") + predictThisYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="今年PE预测") + predictLastYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="去年EPS预测") + predictLastYearPe: Mapped[Optional[str]] = mapped_column(String(50), comment="去年PE预测") + actualLastTwoYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="前两年实际EPS") + actualLastYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="去年实际EPS") + industryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="行业代码") + industryName: Mapped[Optional[str]] = mapped_column(String(100), comment="行业名称") + emIndustryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富行业代码") + indvInduCode: Mapped[Optional[str]] = mapped_column(String(50), comment="个性化行业代码") + indvInduName: Mapped[Optional[str]] = mapped_column(String(100), comment="个性化行业名称") + emRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富评级代码") + emRatingValue: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富评级值") + emRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="东方财富评级名称") + lastEmRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="上次东方财富评级代码") + lastEmRatingValue: Mapped[Optional[str]] = mapped_column(String(50), comment="上次东方财富评级值") + lastEmRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="上次东方财富评级名称") + ratingChange: Mapped[Optional[str]] = mapped_column(String(50), comment="评级变动") + reportType: Mapped[Optional[int]] = mapped_column(Integer, comment="报告类型") + author: Mapped[Optional[str]] = mapped_column(String(255), comment="作者(逗号分隔)") + indvIsNew: Mapped[Optional[str]] = mapped_column(String(20), comment="是否为新研报") + researcher: Mapped[Optional[str]] = mapped_column(String(255), comment="研究员") + newListingDate: Mapped[Optional[str]] = mapped_column(String(50), comment="新上市日期") + newPurchaseDate: Mapped[Optional[str]] = mapped_column(String(50), comment="新买入日期") + newIssuePrice: Mapped[Optional[str]] = mapped_column(String(50), comment="新发行价格") + newPeIssueA: Mapped[Optional[str]] = mapped_column(String(50), comment="新发行市盈率A") + indvAimPriceT: Mapped[Optional[str]] = mapped_column(String(50), comment="目标价上限") + indvAimPriceL: Mapped[Optional[str]] = mapped_column(String(50), comment="目标价下限") + attachType: Mapped[Optional[str]] = mapped_column(String(50), comment="附件类型") + attachSize: Mapped[Optional[int]] = mapped_column(Integer, comment="附件大小") + attachPages: Mapped[Optional[int]] = mapped_column(Integer, comment="附件页数") + encodeUrl: Mapped[Optional[str]] = mapped_column(String(512), comment="加密URL") + sRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="评级名称") + sRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="评级代码") + market: Mapped[Optional[str]] = mapped_column(String(50), comment="市场") + authorID: Mapped[Optional[str]] = mapped_column(String(255), comment="作者ID(逗号分隔)") + count_all: Mapped[Optional[int]] = mapped_column(Integer, comment="计数") + orgType: Mapped[Optional[str]] = mapped_column(String(50), comment="机构类型") + created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), comment="创建时间") + updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间") + + +class ReportsNewstrock(Base): + __tablename__ = "reports_newstrock" # 注意:原表名可能存在笔误(应为newstock) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="自增主键") + infoCode: Mapped[str] = mapped_column(String(255), unique=True, comment="报告唯一标识") + title: Mapped[Optional[str]] = mapped_column(String(512), comment="报告标题") + stockName: Mapped[Optional[str]] = mapped_column(String(100), comment="股票名称") + stockCode: Mapped[Optional[str]] = mapped_column(String(50), comment="股票代码") + orgCode: Mapped[Optional[str]] = mapped_column(String(50), comment="机构代码") + orgName: Mapped[Optional[str]] = mapped_column(String(255), comment="机构全称") + orgSName: Mapped[Optional[str]] = mapped_column(String(100), comment="机构简称") + publishDate: Mapped[Optional[str]] = mapped_column(String(50), comment="发布日期") + curr_column: Mapped[Optional[str]] = mapped_column(String(255), comment="栏目") # 注意:column是SQL关键字 + actualLastTwoYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="前两年实际EPS") + actualLastYearEps: Mapped[Optional[str]] = mapped_column(String(50), comment="去年实际EPS") + industryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="行业代码") + industryName: Mapped[Optional[str]] = mapped_column(String(100), comment="行业名称") + emIndustryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富行业代码") + indvInduCode: Mapped[Optional[str]] = mapped_column(String(50), comment="个性化行业代码") + indvInduName: Mapped[Optional[str]] = mapped_column(String(100), comment="个性化行业名称") + emRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富评级代码") + emRatingValue: Mapped[Optional[str]] = mapped_column(String(50), comment="东方财富评级值") + emRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="东方财富评级名称") + lastEmRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="上次东方财富评级代码") + lastEmRatingValue: Mapped[Optional[str]] = mapped_column(String(50), comment="上次东方财富评级值") + lastEmRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="上次东方财富评级名称") + ratingChange: Mapped[Optional[str]] = mapped_column(String(50), comment="评级变动") + reportType: Mapped[Optional[int]] = mapped_column(Integer, comment="报告类型") + author: Mapped[Optional[str]] = mapped_column(String(255), comment="作者(逗号分隔)") + indvIsNew: Mapped[Optional[str]] = mapped_column(String(20), comment="是否为新研报") + researcher: Mapped[Optional[str]] = mapped_column(String(255), comment="研究员") + newListingDate: Mapped[Optional[str]] = mapped_column(String(50), comment="新上市日期") + newPurchaseDate: Mapped[Optional[str]] = mapped_column(String(50), comment="新买入日期") + newIssuePrice: Mapped[Optional[float]] = mapped_column(Float, comment="新发行价格") + newPeIssueA: Mapped[Optional[float]] = mapped_column(Float, comment="新发行市盈率A") + indvAimPriceT: Mapped[Optional[str]] = mapped_column(String(50), comment="目标价上限") + indvAimPriceL: Mapped[Optional[str]] = mapped_column(String(50), comment="目标价下限") + attachType: Mapped[Optional[str]] = mapped_column(String(50), comment="附件类型") + attachSize: Mapped[Optional[int]] = mapped_column(Integer, comment="附件大小") + attachPages: Mapped[Optional[int]] = mapped_column(Integer, comment="附件页数") + encodeUrl: Mapped[Optional[str]] = mapped_column(String(512), comment="加密URL") + sRatingName: Mapped[Optional[str]] = mapped_column(String(100), comment="评级名称") + sRatingCode: Mapped[Optional[str]] = mapped_column(String(50), comment="评级代码") + market: Mapped[Optional[str]] = mapped_column(String(50), comment="市场") + newStockSort: Mapped[Optional[str]] = mapped_column(String(50), comment="新股分类") + authorID: Mapped[Optional[str]] = mapped_column(String(255), comment="作者ID(逗号分隔)") + count_all: Mapped[Optional[int]] = mapped_column(Integer, comment="计数") + orgType: Mapped[Optional[str]] = mapped_column(String(50), comment="机构类型") + created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), comment="创建时间") + updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间") + + +class ReportsStrategy(Base): + __tablename__ = "reports_strategy" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="自增主键") + infoCode: Mapped[str] = mapped_column(String(255), unique=True, comment="报告唯一标识") + title: Mapped[Optional[str]] = mapped_column(String(512), comment="报告标题") + author: Mapped[Optional[str]] = mapped_column(String(255), comment="作者(逗号分隔)") + orgName: Mapped[Optional[str]] = mapped_column(String(255), comment="机构全称") + orgCode: Mapped[Optional[str]] = mapped_column(String(50), comment="机构代码") + orgSName: Mapped[Optional[str]] = mapped_column(String(100), comment="机构简称") + publishDate: Mapped[Optional[str]] = mapped_column(String(50), comment="发布日期") + encodeUrl: Mapped[Optional[str]] = mapped_column(String(512), comment="加密URL") + researcher: Mapped[Optional[str]] = mapped_column(String(255), comment="研究员") + market: Mapped[Optional[str]] = mapped_column(String(50), comment="市场") + industryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="行业代码") + industryName: Mapped[Optional[str]] = mapped_column(String(100), comment="行业名称") + authorID: Mapped[Optional[str]] = mapped_column(String(255), comment="作者ID(逗号分隔)") + count_all: Mapped[Optional[int]] = mapped_column(Integer, comment="计数") + orgType: Mapped[Optional[str]] = mapped_column(String(50), comment="机构类型") + created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), comment="创建时间") + updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间") + stockName: Mapped[str] = mapped_column(String(100), default="", nullable=False, comment="股票名称(默认空字符串)") + stockCode: Mapped[str] = mapped_column(String(50), default="", nullable=False, comment="股票代码(默认空字符串)") + attachPages: Mapped[Optional[int]] = mapped_column(Integer, comment="附件页数") + + +class ReportsMacresearch(Base): + __tablename__ = "reports_macresearch" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="自增主键") + infoCode: Mapped[str] = mapped_column(String(255), unique=True, comment="报告唯一标识") + json_id: Mapped[Optional[str]] = mapped_column(String(50), comment="JSON ID") + title: Mapped[Optional[str]] = mapped_column(String(512), comment="报告标题") + author: Mapped[Optional[str]] = mapped_column(String(255), comment="作者(逗号分隔)") + orgName: Mapped[Optional[str]] = mapped_column(String(255), comment="机构全称") + orgCode: Mapped[Optional[str]] = mapped_column(String(50), comment="机构代码") + orgSName: Mapped[Optional[str]] = mapped_column(String(100), comment="机构简称") + publishDate: Mapped[Optional[str]] = mapped_column(String(50), comment="发布日期") + encodeUrl: Mapped[Optional[str]] = mapped_column(String(512), comment="加密URL") + researcher: Mapped[Optional[str]] = mapped_column(String(255), comment="研究员") + market: Mapped[Optional[str]] = mapped_column(String(50), comment="市场") + industryCode: Mapped[Optional[str]] = mapped_column(String(50), comment="行业代码") + industryName: Mapped[Optional[str]] = mapped_column(String(100), comment="行业名称") + authorID: Mapped[Optional[str]] = mapped_column(String(255), comment="作者ID(逗号分隔)") + count_all: Mapped[Optional[int]] = mapped_column(Integer, comment="计数") + orgType: Mapped[Optional[str]] = mapped_column(String(50), comment="机构类型") + created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), comment="创建时间") + updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间") + stockCode: Mapped[str] = mapped_column(String(50), default="", nullable=False, comment="股票代码(默认空字符串)") + stockName: Mapped[str] = mapped_column(String(100), default="", nullable=False, comment="股票名称(默认空字符串)") + attachPages: Mapped[Optional[int]] = mapped_column(Integer, comment="附件页数")