Initial commit

This commit is contained in:
P3ngSaM 2023-05-05 15:08:40 +08:00
commit 17a4bc8994
31 changed files with 668 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# 忽略venv文件夹
venv/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
health_service_api

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.11 (health_service_api)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,54 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="NonAsciiCharacters" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="pandas" />
<item index="1" class="java.lang.String" itemvalue="reportlab" />
<item index="2" class="java.lang.String" itemvalue="gevent" />
<item index="3" class="java.lang.String" itemvalue="numpy" />
<item index="4" class="java.lang.String" itemvalue="Django" />
<item index="5" class="java.lang.String" itemvalue="tzdata" />
<item index="6" class="java.lang.String" itemvalue="asgiref" />
<item index="7" class="java.lang.String" itemvalue="python-multipart" />
<item index="8" class="java.lang.String" itemvalue="sqlparse" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
<option value="E127" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyProtectedMemberInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="str.*" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (health_service_api)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/health_service_api.iml" filepath="$PROJECT_DIR$/.idea/health_service_api.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,433 @@
import ast
import copy
import json
import os
import re
import time
from pathlib import Path
from typing import Dict
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
import openpyxl
import pandas as pd
import numpy as np
from interval import Interval
from openpyxl.utils import get_column_letter
from pyuca import Collator
from starlette import status
from starlette.responses import FileResponse
from APP.Schemas import ComputeSchemas
from Utils.DataBase.MongoHelperUtils import MongoHelper, get_mongodb
router = APIRouter(
prefix="/api/health_industry"
)
def calculate_expression(expression: ast.Expression, params: Dict) -> float:
"""
使用AST模块计算安全的表达式
"""
try:
code = compile(expression, "<string>", "eval")
result = eval(code, {}, params)
return result
except SyntaxError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="公式语法错误:" + str(e))
except NameError as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="公式中存在未定义的变量:" + str(e))
except TypeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="公式中的某些操作不支持所提供的参数类型:" + str(e))
except Exception as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="执行公式时出现错误:" + str(e))
@router.post("/upload_model_excel", summary="上传模型excel", tags=["模型仓库"])
async def func(file: UploadFile = File(...), mongodb: MongoHelper = Depends(get_mongodb)):
contents = await file.read()
data = dict()
xls = pd.read_excel(contents, sheet_name=None)
for sheet_name, df in xls.items():
if sheet_name == '得分级别标准':
data['得分级别标准'] = df.set_index('得分级别标准')['Unnamed: 1'].to_dict()
elif sheet_name == '档位得分标准':
data['档位得分标准'] = df.set_index('档位得分标准')['Unnamed: 1'].to_dict()
elif sheet_name == '级别调整':
data['级别调整'] = df.set_index('级别调整').index.to_list()
else:
df = df.replace(np.nan, None)
search = mongodb.find_data_by_condition(dbname="打分模型", sheet="模型集",
condition={"模型名称": sheet_name})
if search:
raise HTTPException(status_code=202, detail="模型已存在")
data['模型名称'] = sheet_name
data_list = []
# 补充df指标缺失值
df.loc[:, '一级指标'] = df['一级指标'].fillna(method="ffill")
df.loc[:, '二级指标'] = df['二级指标'].fillna(method="ffill")
df.loc[:, '三级指标'] = df['三级指标'].fillna(method="ffill")
df.loc[:, '四级指标'] = df['四级指标'].fillna(method="ffill")
for index, row in df.iterrows():
data_list.append(row.to_dict())
data['指标数据'] = data_list
mongodb.insert_data(dbname="打分模型", sheet="模型集", data=data)
return {
"code": 200,
"message": "上传成功"
}
@router.post("/download", summary="导出模型填报数据", tags=["模型仓库"])
def func(name: str = '模型名称', year: str = "2023", mongodb: MongoHelper = Depends(get_mongodb)):
# 根据名称查询模型是否存在
search = mongodb.find_data_by_condition(dbname="打分模型", sheet="模型集", condition={"模型名称": name})
if not search:
raise HTTPException(status_code=202, detail="模型不存在")
index = search.get('指标数据')
# 参数集
params = list()
# 清洗所有公式需要的参数值
for item in index:
formula = item.get('计算公式')
if isinstance(formula, str):
pattern = re.compile('[\u4e00-\u9fa5]+')
matches = pattern.findall(formula)
for match in matches:
params.append(match)
params = list(set(params))
param_dict = dict()
for param in params:
param_dict[param] = None
year_dict = dict()
year_dict[year] = param_dict
excel = os.path.join(os.getcwd(), 'Utils', 'File', 'template', '填报模板.xlsx')
# 生成财务数据填报excel
sheet_01 = pd.DataFrame(year_dict)
# 创建ExcelWriter对象
writer = pd.ExcelWriter(excel)
sheet_01.to_excel(writer, sheet_name='填报数据表')
writer.close()
file_path = Path(excel)
wb = openpyxl.load_workbook(file_path)
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
for column in range(1, sheet.max_column + 1):
letter = get_column_letter(column)
max_length = 0
for cell in sheet[letter]:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except Exception:
pass
adjusted_width = (max_length + 18)
sheet.column_dimensions[letter].width = adjusted_width
wb.save(file_path)
return FileResponse(file_path, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=file_path.name)
@router.post("/upload", summary="上传填报数据excel", tags=["模型仓库"])
async def func(file: UploadFile = File(...)):
data = dict()
contents = await file.read()
xls = pd.read_excel(contents, sheet_name=None)
for sheet_name, df in xls.items():
df = df.set_index('Unnamed: 0')
df.columns.name = 'Year'
df.columns = df.columns.str.strip()
result = dict()
for col in df.columns:
if df[col].isna().any().any():
raise HTTPException(status_code=400, detail="数据未填报完整")
result[str(col)] = df[col].to_dict()
data[sheet_name] = result
return {
"code": 200,
"message": "上传成功",
"content": data
}
@router.post("/index_calculate", summary="指标计算", tags=["模型仓库"])
def func(schemas: ComputeSchemas.ComputeModelReqBody, mongodb: MongoHelper = Depends(get_mongodb)):
search = mongodb.find_data_by_condition(dbname="打分模型", sheet="模型集", condition={"模型名称": schemas.name})
if not search:
raise HTTPException(status_code=202, detail="模型不存在")
index = search.get('指标数据')
data = schemas.index
result = dict()
# 计算该模型共有几层指标
index_list = list()
for key in index[0].keys():
if '指标' in key:
index_list.append(key.replace("级指标", ""))
collator = Collator()
sorted_words = sorted(index_list, key=collator.sort_key)
min_index = sorted_words[-1] + '级指标'
# 遍历指标定性指标直接返回None定量指标进行计算
for item in index:
name = item.get(min_index)
formula = item.get('计算公式')
if isinstance(formula, str) and formula != '-':
# 获取指标值
params = dict()
pattern = re.compile('[\u4e00-\u9fa5]+')
matches = pattern.findall(formula)
for match in matches:
params[match] = data.get(match)
expression_ast = ast.parse(formula, mode="eval")
result[name] = str(round(calculate_expression(expression_ast, params), 2))
else:
result[name] = None
return {
"code": 200,
"message": "计算完成",
"content": result
}
@router.post("/score_calculate", summary="计算得分", tags=["模型仓库"])
def func(schemas: ComputeSchemas.ComputeModelReqBody, mongodb: MongoHelper = Depends(get_mongodb)):
search = mongodb.find_data_by_condition(dbname="打分模型", sheet="模型集", condition={"模型名称": schemas.name})
if not search:
raise HTTPException(status_code=202, detail="模型不存在")
index_data = search.get('指标数据')
score_standard = search.get('得分级别标准')
gear_positio = search.get('档位得分标准')
# 计算该模型共有几层指标
index_list = list()
for key in index_data[0].keys():
if '指标' in key:
index_list.append(key.replace("级指标", ""))
collator = Collator()
sorted_words = sorted(index_list, key=collator.sort_key)
min_index = sorted_words[-1] + '级指标'
# 指标数值数据
data = schemas.index
def score_func(lev, wei):
num = gear_positio.get(lev)
return round(num / 100 * wei, 2)
index_score = list()
for index in index_data:
index_dict = copy.deepcopy(index)
# 指标名称
index_name = index.get(min_index)
# 指标数值
value = data.get(index_name)
index_dict['数值'] = data.get(index_name)
# 指标权重
weight = index.get('权重')
# 指标数值分为四种:常规数值型、直接进在区间内进行计算,部分区间可能缺失;财务指标数值型,经过计算的财务指标数值直接在区间进行计算;字符串型,直接与档位匹配,布尔型:是与否,一档与八档
if isinstance(value, str):
# 遍历档位
for key, val in index.items():
if '' in key and value == val:
index_dict['得分'] = score_func(key, weight)
index_dict['档位'] = key
index_score.append(index_dict)
elif isinstance(value, float) or isinstance(value, int):
for gear_key, gear_value in index.items():
if '' in gear_key:
try:
values = gear_value
begin = values[0]
end = values[-1]
number = values[1:-1]
def judge_type(num):
if num == '+inf':
num = float('inf')
elif num == '-inf':
num = float('-inf')
else:
num = float(num)
return num
begin_numeber = judge_type(number[0:number.rfind(',')])
end_numeber = judge_type(number[number.rfind(','):].replace(",", "").strip())
# 区间各种情况
zoom = None
if begin == "(" and end == ")":
zoom = Interval(begin_numeber, end_numeber, closed=False)
elif begin == "(" and end == "]":
zoom = Interval(begin_numeber, end_numeber, lower_closed=False)
elif begin == "[" and end == "]":
zoom = Interval(begin_numeber, end_numeber)
elif begin == "[" and end == ")":
zoom = Interval(begin_numeber, end_numeber, upper_closed=False)
if float(value) in zoom:
index_dict['得分'] = score_func(gear_key, weight)
index_dict['档位'] = gear_key
index_score.append(index_dict)
break
except Exception:
index_dict['得分'] = score_func(gear_key, weight)
index_dict['档位'] = gear_key
index_score.append(index_dict)
break
# 汇总得分
score = round(sum(item.get('得分') for item in index_score), 2)
rank_level = 'C'
for le, s in score_standard.items():
if score > s:
rank_level = le
break
return {
"code": 200,
"message": "计算成功",
"content": {
"level": rank_level,
"total_score": score,
"indicator": index_score
}
}
@router.post("/batch_score_calculate", summary="批量计算得分(测试用)", tags=["模型仓库"])
async def func(name: str = "模型名称", file: UploadFile = File(...), mongodb: MongoHelper = Depends(get_mongodb)):
search = mongodb.find_data_by_condition(dbname="打分模型", sheet="模型集", condition={"模型名称": name})
if not search:
raise HTTPException(status_code=202, detail="模型不存在")
index_data = search.get('指标数据')
score_standard = search.get('得分级别标准')
gear_positio = search.get('档位得分标准')
# 计算该模型共有几层指标
index_list = list()
for key in index_data[0].keys():
if '指标' in key:
index_list.append(key.replace("级指标", ""))
collator = Collator()
sorted_words = sorted(index_list, key=collator.sort_key)
min_index = sorted_words[-1] + '级指标'
# 读取excel文件
contents = await file.read()
data_list = []
xls = pd.read_excel(contents, sheet_name=None)
for sheet_name, df in xls.items():
if sheet_name == '测试企业':
for index, row in df.iterrows():
data_list.append(row.to_dict())
def score_func(lev, wei):
num = gear_positio.get(lev)
return round(num / 100 * wei, 2)
def score_level_func(res_score):
rank_level = 'C'
for le, s in score_standard.items():
if res_score > s:
rank_level = le
break
return rank_level
# 遍历公司数据
result_list = list()
for item in data_list:
result_dict = dict()
result_dict['企业名称'] = item.get('公司中文名称')
result_dict['证券代码'] = item.get('证券代码')
result_dict['证券简称'] = item.get('证券简称')
result_dict['分类'] = item.get('分类')
# 遍历指标打分
index_score = list()
for index in index_data:
index_dict = copy.deepcopy(index)
# 指标名称
index_name = index.get(min_index)
# 指标数值
value = item.get(index_name)
index_dict['数值'] = item.get(index_name)
# 指标权重
weight = index.get('权重')
# 指标数值分为四种:常规数值型、直接进在区间内进行计算,部分区间可能缺失;财务指标数值型,经过计算的财务指标数值直接在区间进行计算;字符串型,直接与档位匹配,布尔型:是与否,一档与八档
if isinstance(value, str):
# 遍历档位
for key, val in index.items():
if '' in key and value == val:
index_dict['得分'] = score_func(key, weight)
index_dict['档位'] = key
index_score.append(index_dict)
elif isinstance(value, float) or isinstance(value, int):
for gear_key, gear_value in index.items():
if '' in gear_key:
try:
values = gear_value
begin = values[0]
end = values[-1]
number = values[1:-1]
def judge_type(num):
if num == '+inf':
num = float('inf')
elif num == '-inf':
num = float('-inf')
else:
num = float(num)
return num
begin_numeber = judge_type(number[0:number.rfind(',')])
end_numeber = judge_type(number[number.rfind(','):].replace(",", "").strip())
# 区间各种情况
zoom = None
if begin == "(" and end == ")":
zoom = Interval(begin_numeber, end_numeber, closed=False)
elif begin == "(" and end == "]":
zoom = Interval(begin_numeber, end_numeber, lower_closed=False)
elif begin == "[" and end == "]":
zoom = Interval(begin_numeber, end_numeber)
elif begin == "[" and end == ")":
zoom = Interval(begin_numeber, end_numeber, upper_closed=False)
if float(value) in zoom:
index_dict['得分'] = score_func(gear_key, weight)
index_dict['档位'] = gear_key
index_score.append(index_dict)
break
except Exception:
index_dict['得分'] = score_func(gear_key, weight)
index_dict['档位'] = gear_key
index_score.append(index_dict)
break
result_dict['指标'] = index_score
score = sum(i.get('得分') for i in index_score)
result_dict['总分'] = sum(i.get('权重') for i in index_score)
result_dict['得分'] = score
result_dict['级别'] = score_level_func(score)
print(result_dict)
time.sleep(1)
result_dict.pop('指标')
result_list.append(result_dict)
df = pd.DataFrame(result_list)
df.to_excel('测试数据打分结果.xlsx', index=False)
return result_list

0
APP/Router/__init__.py Normal file
View File

Binary file not shown.

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel
class ComputeModelReqBody(BaseModel):
name: str = "模型名称"
index: dict = {}

0
APP/Schemas/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

0
APP/__init__.py Normal file
View File

Binary file not shown.

View File

@ -0,0 +1,9 @@
{
"Mysql": {
"wr_report_flow": "mysql+pymysql://root:123456@localhost/wr_report_flow?charset=utf8mb4"
},
"MongoDB": {
"test": "root:123456@116.63.159.166:27017",
"local": "localhost:27017"
}
}

View File

@ -0,0 +1,89 @@
import re
import os
import json
import gridfs
import pymongo
from urllib import parse
from bson import ObjectId
class MongoHelper:
def __init__(self, param):
"""
param:
typestr
desc: 选择连接哪个MongoDB数据库
"""
with open(os.path.abspath(os.path.dirname(__file__) + '/DBConfig.json')) as f:
db_configs = json.load(f)
this_mongo_cfg = db_configs['MongoDB'][param]
m = re.match(r'([\s\S].*?):([\s\S].*)@([\s\S].*)', this_mongo_cfg)
parsed_mongo_config = "{}:{}@{}".format(parse.quote_plus(m.group(1)), parse.quote_plus(m.group(2)), m.group(3))
self.client = pymongo.MongoClient('mongodb://{}'.format(parsed_mongo_config))
def insert_data(self, dbname: str, sheet: str, data: dict):
collection = self.client[dbname][sheet]
item = collection.insert_one(data)
return item.inserted_id.__str__()
def delete_data_by_id(self, dbname: str, sheet: str, _id: str):
collection = self.client[dbname][sheet]
collection.delete_one({'_id': ObjectId(_id)})
return True
def find_data_by_id(self, dbname: str, sheet: str, _id: str):
collection = self.client[dbname][sheet]
return collection.find_one({'_id': ObjectId(_id)}, {"_id": False})
def find_data_by_name(self, dbname: str, sheet: str, name: str):
collection = self.client[dbname][sheet]
return collection.find_one({'name': name}, {"_id": False})
def find_data_by_condition(self, dbname: str, sheet: str, condition: dict):
collection = self.client[dbname][sheet]
return collection.find_one(condition, {"_id": False})
def query_data(self, dbname: str, sheet: str, name: str, page: int, pagesize: int):
collection = self.client[dbname][sheet]
if name:
query = {'name': name}
else:
query = {}
total_count = collection.count_documents(query)
results = collection.find(query, {"_id": False}).skip((page - 1) * pagesize).limit(pagesize)
return list(results), total_count
def delete_data_by_name(self, dbname: str, sheet: str, name: str):
collection = self.client[dbname][sheet]
collection.delete_one({'name': name})
return True
def update_data_by_id(self, dbname: str, sheet: str, _id: str, data: dict):
collection = self.client[dbname][sheet]
collection.update_one({'_id': ObjectId(_id)}, {"$set": data})
return True
def update_data_by_name(self, dbname: str, sheet: str, name: str, data: dict):
collection = self.client[dbname][sheet]
collection.update_one({'name': name}, {"$set": data})
return True
def insert_file(self, file: bytes):
fs = gridfs.GridFS(self.client['评级报告'], 'docx')
return fs.put(file)
def get_file(self, fid: str):
fs = gridfs.GridFS(self.client['评级报告'], 'docx')
gf = fs.get(ObjectId(fid))
return gf.read()
def get_mongodb():
try:
db = MongoHelper("test")
yield db
finally:
db.client.close()

View File

Binary file not shown.

0
Utils/File/__init__.py Normal file
View File

Binary file not shown.

0
Utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

21
main.py Normal file
View File

@ -0,0 +1,21 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from APP.Router import HealthIndustryRouter
app = FastAPI(
title="保健服务后端接口",
description="保健服务评级打分流程相关",
version="v1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(HealthIndustryRouter.router)

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
fastapi==0.95.1
interval==1.0.0
numpy==1.24.2
openpyxl==3.1.2
pandas==2.0.1
pydantic==1.10.7
pymongo==4.3.3
pyuca==1.2
starlette==0.26.1
python-multipart==0.0.6