├── .gitignore ├── 20230216175547.jpg ├── Pipfile ├── README.md ├── application ├── __init__.py ├── blueprint │ ├── __init__.py │ ├── web │ │ ├── __init__.py │ │ └── chat.py │ └── wechat │ │ ├── __init__.py │ │ └── views.py ├── cli.py ├── config │ ├── __init__.py │ ├── default.py │ ├── development.py │ ├── local.py │ ├── production.py │ └── testing.py ├── contants.py ├── contrib │ ├── cache.py │ └── openai_api.py ├── dto │ ├── __init__.py │ ├── base.py │ └── wechat.py ├── enum_field.py ├── errors.py ├── extensions.py ├── middlewares.py ├── models │ ├── __init__.py │ └── base.py ├── services │ ├── __init__.py │ └── openai_service.py ├── tasks │ └── __init__.py ├── templates │ ├── chatgpt.html │ └── company.html └── utils │ ├── __init__.py │ ├── aes_cipher.py │ ├── base64_utils.py │ ├── country_utils.py │ ├── json_utils.py │ ├── jwt_utils.py │ ├── list_utils.py │ ├── md5_utils.py │ ├── parse_utils.py │ ├── random_utils.py │ ├── request_utils.py │ ├── response_utils.py │ └── time_utils.py ├── gunicorn.conf ├── image-20230216152404777.png ├── qrcode_gh_4340d45cdd5f_1.jpg ├── requirements.txt ├── tests ├── __init__.py ├── base │ ├── __init__.py │ └── http.py └── extension │ └── __init__.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | *.xlsx 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | *whl 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | *.db 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | .idea/ 142 | *.pdf 143 | ~* 144 | *.log 145 | .env 146 | .flaskenv 147 | supervisor 148 | f-19-1-8-03.opus -------------------------------------------------------------------------------- /20230216175547.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/20230216175547.jpg -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | alembic = "==1.4.3" 10 | aliyun-python-sdk-cloudauth = "==2.0.26" 11 | aliyun-python-sdk-core = "==2.13.30" 12 | aliyun-python-sdk-core-v3 = "==2.13.11" 13 | aliyun-python-sdk-kms = "==2.13.0" 14 | amqp = "==5.0.2" 15 | aniso8601 = "==8.1.0" 16 | bcrypt = "==3.2.0" 17 | beautifulsoup4 = "==4.9.3" 18 | bidict = "==0.21.2" 19 | billiard = "==3.6.3.0" 20 | celery = "==5.0.3" 21 | certifi = "==2020.12.5" 22 | cffi = "==1.14.4" 23 | chardet = "==3.0.4" 24 | click = "==7.1.2" 25 | click-didyoumean = "==0.0.3" 26 | click-plugins = "==1.1.1" 27 | click-repl = "==0.1.6" 28 | crcmod = "==1.7" 29 | cryptography = "==3.2.1" 30 | et-xmlfile = "==1.0.1" 31 | faker = "==5.0.0" 32 | flask-bcrypt = "==0.7.1" 33 | flask-redis = "==0.4.0" 34 | flask-wechatpy = "==0.1.3" 35 | gevent = "==21.1.1" 36 | gevent-websocket = "==0.10.1" 37 | google = "==3.0.0" 38 | greenlet = "==1.0.0" 39 | gunicorn = "==20.0.4" 40 | idna = "==2.10" 41 | inflection = "==0.5.1" 42 | itsdangerous = "==1.1.0" 43 | jdcal = "==1.4.1" 44 | jinja2 = "==2.11.2" 45 | jmespath = "==0.10.0" 46 | jpush = "==3.3.8" 47 | kombu = "==5.0.2" 48 | lml = "==0.1.0" 49 | openpyxl = "==3.0.5" 50 | optionaldict = "==0.1.2" 51 | oss2 = "==2.13.1" 52 | prompt-toolkit = "==3.0.8" 53 | protobuf = "==3.14.0" 54 | pycparser = "==2.20" 55 | pycryptodome = "==3.9.9" 56 | pyexcel = "==0.6.6" 57 | pyexcel-io = "==0.6.4" 58 | pyexcel-webio = "==0.1.4" 59 | pyexcel-xlsx = "==0.6.0" 60 | pyhumps = "==1.6.1" 61 | python-dateutil = "==2.8.1" 62 | python-editor = "==1.0.4" 63 | python-engineio = "==3.14.2" 64 | python-socketio = "==4.6.1" 65 | pytz = "==2020.4" 66 | redis = "==3.5.3" 67 | requests = "==2.25.0" 68 | six = "==1.15.0" 69 | soupsieve = "==2.1" 70 | text-unidecode = "==1.3" 71 | texttable = "==1.6.3" 72 | urllib3 = "==1.26.2" 73 | vine = "==5.0.0" 74 | wcwidth = "==0.2.5" 75 | wechatpy = "==1.8.14" 76 | xmltodict = "==0.12.0" 77 | hashids = "*" 78 | aliyun-python-sdk-vod = "*" 79 | bson = "==0.5.10" 80 | flask-restx = "==0.5.1" 81 | qcloud-python-sts = "==3.0.5" 82 | xlsxwriter = "==3.0.2" 83 | cos-python-sdk-v5 = "*" 84 | tencentcloud-sdk-python = "==3.0.581" 85 | sentry-sdk = {extras = ["flask"],version = "==1.5.6"} 86 | Cerberus = "==1.3.2" 87 | Flask = "==1.1.2" 88 | Flask-Excel = "==0.0.7" 89 | Flask-JWT-Extended = "==3.25.0" 90 | Flask-Migrate = "==2.5.3" 91 | Flask-RESTful = "==0.3.8" 92 | Flask-SocketIO = "==4.3.2" 93 | Flask-SQLAlchemy = "==2.4.4" 94 | Flask-Testing = "==0.8.0" 95 | Mako = "==1.1.3" 96 | MarkupSafe = "==1.1.1" 97 | PyJWT = "==1.7.1" 98 | SQLAlchemy = "==1.3.20" 99 | SQLAlchemy-Utils = "==0.36.8" 100 | Werkzeug = "==1.0.1" 101 | Pillow = "==8.4.0" 102 | 103 | [requires] 104 | python_version = "3.7" 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 基于公众号的chatgpt 4 | 5 | 该项目可以让你在公众号实现与chatgpt的对话 6 | 7 | ### 技术栈 8 | * web框架:Flask 9 | * wsgi server: gunicorn 10 | 11 | ### 前置操作 12 | 13 | 1. 安装 python3.8以上版本 14 | 2. 安装依赖文件 15 | ```commandline 16 | pip install -r requirements.txt 17 | 18 | ``` 19 | 20 | 21 | 22 | ### 本地开发 23 | 24 | 1、在项目根目录创建 `.env`文件 ,主要放置敏感数据,比如数据库配置,密钥等数据,内容: 25 | 26 | ``` 27 | WECHAT_APPID=公众号APPID 28 | WECHAT_SECRET=公众号密钥 29 | OPENAI_KEY=chatpgt的openapi key 30 | REDIS_PASSWORD=Redis密码 31 | ``` 32 | 33 | 2、启动 34 | 35 | ``` 36 | flask run 37 | ``` 38 | 39 | 正式环境中请将配置项放置在环境变量中,教程请参考文章:[我用Python写个公众号版chatgpt:打造私人AI助理](https://mp.weixin.qq.com/s/-zhfsvF6ENzMka7Wk6hyMA) 40 | 41 | 42 | 43 | 运行效果 44 | 45 | ![](./image-20230216152404777.png) 46 | 47 | 48 | 49 | ### 体验地址 50 | 51 | 关注公众号【志军foofish】直接发起提问 52 | 53 | ![](qrcode_gh_4340d45cdd5f_1.jpg) 54 | 55 | 56 | 57 | 58 | ### 联系我 59 | 60 | 微信:lzjun567 61 | 62 | ![](./20230216175547.jpg) -------------------------------------------------------------------------------- /application/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging.config 3 | import os 4 | import sys 5 | import time 6 | from importlib import import_module 7 | from logging import StreamHandler 8 | from flask.json import JSONEncoder 9 | import enum 10 | import decimal 11 | from pydantic import BaseModel as PydanticModel 12 | from flask import Flask, request, g 13 | from flask_siwadoc import ValidationError 14 | from werkzeug.exceptions import HTTPException 15 | 16 | from application.cli import configure_cli 17 | from application.config import config, BaseConfig 18 | from application.utils import request_utils 19 | from application.utils.response_utils import error 20 | 21 | __all__ = ['create_app'] 22 | 23 | 24 | def create_app(config_name=None, app_name=None): 25 | """使用工厂模式创建app""" 26 | if not app_name: 27 | app_name = __name__ 28 | app = Flask(app_name) 29 | configure_app(app, config_name) 30 | configure_logging(app) 31 | config_blueprint(app) 32 | config_extensions(app) 33 | configure_hook(app) 34 | configure_json_decode(app) 35 | configure_errors(app) 36 | configure_cli(app) 37 | 38 | return app 39 | 40 | 41 | def configure_app(app, config_name=None): 42 | if not config_name: 43 | config_name = os.getenv('FLASK_ENV', 'development') 44 | app.config.from_object(config[config_name]) 45 | 46 | 47 | def config_blueprint(app): 48 | from application.blueprint.wechat import wechat_bp 49 | from application.blueprint.web import web_bp 50 | app.register_blueprint(wechat_bp) 51 | app.register_blueprint(web_bp) 52 | 53 | 54 | def config_extensions(app): 55 | """ 56 | 配置扩展 57 | """ 58 | # 在方法里面import主要是为了先配置logging,否则在下面模块中如有引用了logging,那么在app设置的logger配置将不在生效 59 | from application.extensions import siwa, openai_api, cache 60 | # 根据models生成rest api前, 必须import 所有model 61 | local_all_models(app) 62 | siwa.init_app(app) 63 | openai_api.init_app(app) 64 | cache.init_app(app) 65 | 66 | 67 | 68 | def configure_logging(app): 69 | """配置日志 70 | """ 71 | # 清除掉默认handlers 72 | app.logger.handlers = [] 73 | werkzeug_logger = logging.getLogger('werkzeug') 74 | werkzeug_logger.disabled = True 75 | sqlalchemy_logger = logging.getLogger("sqlalchemy") 76 | sqlalchemy_logger.handlers = [] 77 | sqlalchemy_logger.propagate = False 78 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 79 | app.logger.propagate = False 80 | 81 | stream_handler = StreamHandler(sys.stdout) 82 | log_level_str = app.config.get('LOG_LEVEL', None) or 'DEBUG' 83 | log_level = getattr(logging, log_level_str) 84 | stream_handler.setLevel(log_level) 85 | stream_handler.setFormatter(formatter) 86 | app.logger.addHandler(stream_handler) 87 | api_logger = logging.getLogger("application") 88 | # gunicorn_logger = logging.getLogger('gunicorn.error') 89 | api_logger.handlers = [stream_handler] 90 | # api_logger.handlers.extend(gunicorn_logger.handlers) 91 | api_logger.propagate = False 92 | 93 | 94 | def configure_hook(app): 95 | @app.before_request 96 | def before_request(): 97 | g.start = time.time() 98 | if request.data: 99 | try: 100 | app.logger.info("request body: %s", request.data) 101 | except: 102 | import traceback 103 | traceback.print_exc() 104 | 105 | @app.after_request 106 | def log_response(response): 107 | diff = int((time.time() - g.start) * 1000) 108 | app.logger.info("%s %s %s %s %sms", 109 | *[request_utils.get_client_ip(), request.method, request.full_path, response.status, 110 | diff]) 111 | return response 112 | 113 | 114 | def configure_errors(app): 115 | from application.errors import ApiError 116 | 117 | @app.errorhandler(ApiError) 118 | def api_error(e: ApiError): 119 | app.logger.warning(f"\ncode={e.code}\nmsg:{e.msg} \nerror_info:{e.error_info}") 120 | return error(code=e.code, msg=e.msg, http_code=e.http_code, data=e.data, error_info=e.error_info) 121 | 122 | @app.errorhandler(ValidationError) 123 | def validate_error(e: ValidationError): 124 | app.logger.warning(f"validate error info:{e.errors()}") 125 | return error(code=1002, msg="请求参数错误", http_code=400, data=None, error_info=e.errors()) 126 | 127 | @app.errorhandler(HTTPException) 128 | def http_error(e): 129 | response = e.get_response() 130 | return error(msg=str(e), 131 | http_code=response.status_code, 132 | code=response.status_code) 133 | 134 | @app.errorhandler(Exception) 135 | def server_error(e): 136 | app.logger.error(f"内部错误:{str(e)}", exc_info=True) 137 | return error(code=500, http_code=500, msg="内部错误") 138 | 139 | 140 | def configure_json_decode(app): 141 | class CustomJSONEncoder(JSONEncoder): 142 | 143 | def default(self, obj): 144 | try: 145 | if isinstance(obj, PydanticModel): 146 | return obj.dict() 147 | elif isinstance(obj, enum.Enum): 148 | return obj.value 149 | elif type(obj) is datetime.date: 150 | return datetime.datetime(obj.year, obj.month, obj.day).isoformat() 151 | elif type(obj) is datetime.datetime: 152 | return obj.isoformat() 153 | elif type(obj) is datetime.time: 154 | return str(obj) 155 | elif isinstance(obj, decimal.Decimal): 156 | return float(obj) 157 | # 时间区间 158 | elif isinstance(obj, datetime.timedelta): 159 | return int(obj.total_seconds() * 1000) 160 | else: 161 | return str(obj) 162 | except TypeError: 163 | pass 164 | return JSONEncoder.default(self, obj) 165 | 166 | app.json_encoder = CustomJSONEncoder 167 | 168 | 169 | def local_all_models(app): 170 | """ 171 | 加载所有的models 172 | """ 173 | root_path = app.root_path 174 | models_directory = os.path.join(root_path, 'application/models') 175 | for root, dirs, files in os.walk(models_directory): 176 | namespace = 'application.models' 177 | if 'celery_app.py' not in files: 178 | continue 179 | 180 | root = root.replace(models_directory, "", 1) 181 | path = root.split(os.sep) 182 | for file in files: 183 | if not file.endswith('.py'): 184 | continue 185 | module_paths = [namespace] + path[1:] 186 | if file != 'celery_app.py': 187 | module_name = os.path.splitext(file)[0] 188 | module_paths = module_paths + [module_name] 189 | 190 | model_module = '.'.join(module_paths) 191 | import_module(model_module) 192 | 193 | 194 | def configure_jwt(jwt_ext): 195 | @jwt_ext.user_lookup_loader 196 | def loader_user_callback(jwt_headers, jwt_payload): 197 | identity = jwt_payload.get("sub") 198 | from application.utils.jwt_utils import loader_user 199 | return loader_user(identity) 200 | 201 | @jwt_ext.invalid_token_loader 202 | @jwt_ext.unauthorized_loader 203 | def invalid_jwt_callback(error_string): 204 | return error(msg="无效的token", code=2040, http_code=401) 205 | 206 | @jwt_ext.expired_token_loader 207 | def expired_token_callback(jwt_headers, jwt_payload): 208 | return error(msg="token已过期", code=2040, http_code=401) 209 | 210 | @jwt_ext.user_lookup_error_loader 211 | def default_user_loader_error_callback(jwt_headers, jwt_payload): 212 | """ 213 | """ 214 | return error(msg="用户不存在", code=2040, http_code=401) 215 | -------------------------------------------------------------------------------- /application/blueprint/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /application/blueprint/web/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | web_bp = Blueprint('web', __name__, url_prefix='/web') 4 | 5 | from . import chat -------------------------------------------------------------------------------- /application/blueprint/web/chat.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | from flask import current_app, request, render_template 5 | 6 | from application.extensions import siwa, cache, openai_api 7 | from . import web_bp 8 | 9 | 10 | @web_bp.route("/company", methods=["GET", "POST"]) 11 | def handler_company(): 12 | """ 13 | 根据公司信息生成一句话简介 14 | """ 15 | answer = "" 16 | if request.method == 'POST': 17 | intro = request.form.get("intro") 18 | question = f"'''{intro}'''\n" \ 19 | f"基于上面几段话生成一句话简介,格式:xxx商, 必须以 xxx提供商/服务商/销售商/品牌商/运营商/开发商/生产商/研发商/制造商/供应商/平台商 结尾," \ 20 | f"不得超过20个字符,不要标点符号" 21 | answer = openai_api.answer(question).strip() 22 | return render_template("company.html", answer=answer) 23 | 24 | 25 | @web_bp.get("/") 26 | def chat(): 27 | return render_template("chatgpt.html") 28 | -------------------------------------------------------------------------------- /application/blueprint/wechat/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | wechat_bp = Blueprint('wechat', __name__, url_prefix='/wechat') 4 | 5 | from . import views 6 | -------------------------------------------------------------------------------- /application/blueprint/wechat/views.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | from flask import current_app, request, render_template 5 | from wechatpy.events import SubscribeScanEvent, ScanEvent 6 | from wechatpy.exceptions import InvalidSignatureException 7 | from wechatpy.messages import TextMessage 8 | from wechatpy.replies import TextReply 9 | from wechatpy.utils import check_signature 10 | 11 | from application.extensions import siwa, cache, openai_api 12 | from . import wechat_bp 13 | 14 | from ...dto import ResponseSuccessDto 15 | from ...dto.wechat import SignatureDto 16 | from ...errors import WechatError 17 | from wechatpy import parse_message 18 | 19 | from ...services.openai_service import set_answer 20 | 21 | 22 | @wechat_bp.get('/wechat') 23 | @siwa.doc(query=SignatureDto, resp=ResponseSuccessDto, group="wechat") 24 | def signature_validate(query: SignatureDto): 25 | """ 26 | 微信服务器校验 27 | """ 28 | try: 29 | token = current_app.config.get("WECHAT_TOKEN") 30 | check_signature(token, query.signature, query.timestamp, query.nonce) 31 | return query.echostr 32 | except InvalidSignatureException: 33 | raise WechatError(msg="invalid signature") 34 | 35 | 36 | 37 | 38 | @wechat_bp.post("/wechat") 39 | def handler_wx_msg(): 40 | """ 41 | 处理微信事件 42 | """ 43 | msg = parse_message(request.data) 44 | openid = request.args.get("openid") 45 | current_app.logger.info("openid:%s", openid) 46 | answer = "欢迎使用志军的AI助理,有什么需要帮助的吗?" 47 | if isinstance(msg, (SubscribeScanEvent, ScanEvent)): 48 | # 关注或扫二维码 49 | result = TextReply(content=answer, 50 | message=msg).render() 51 | elif isinstance(msg, TextMessage): 52 | question = msg.content 53 | if question != "继续": 54 | current_app.logger.info(f"问题:{question}") 55 | s = threading.Thread(target=set_answer, args=(openid, question)) 56 | s.start() 57 | time.sleep(2) 58 | answer = cache.pop(openid) 59 | if not answer: 60 | answer = "我正在思考,请稍后回复【继续】获取回答" 61 | else: 62 | answer = cache.pop(openid) 63 | if not answer: 64 | answer = "请稍后,还没准备好回答" 65 | result = TextReply(content=answer, message=msg).render() 66 | else: 67 | result = TextReply(content=answer, message=msg).render() 68 | return result 69 | -------------------------------------------------------------------------------- /application/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | 命令行工具 3 | """ 4 | import logging 5 | import random 6 | from typing import List 7 | 8 | import click 9 | from flask import current_app 10 | from application.enum_field import MaterialType 11 | 12 | logger = logging.getLogger() 13 | logger.setLevel(level=logging.INFO) 14 | 15 | 16 | def configure_cli(app): 17 | 18 | 19 | @app.cli.command("chat") 20 | @click.option("--question", prompt="问题") 21 | def chat_ai(question): 22 | """ 23 | 同步机构用户到营销平台 24 | """ 25 | from application.extensions import openai_api 26 | answer = openai_api.answer(question) 27 | print(answer) 28 | 29 | -------------------------------------------------------------------------------- /application/config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .development import DevelopmentConfig 4 | from .production import ProductionConfig 5 | from .testing import TestingConfig 6 | from .local import LocalConfig 7 | from .default import BaseConfig 8 | 9 | config = { 10 | 'development': DevelopmentConfig, 11 | 'testing': TestingConfig, 12 | 'production': ProductionConfig, 13 | 'local': LocalConfig 14 | } 15 | -------------------------------------------------------------------------------- /application/config/default.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import urllib.parse 4 | import os 5 | 6 | 7 | class BaseConfig: 8 | PROJECT = "api" 9 | PROFILE = True 10 | ENV = "development" 11 | PROJECT_PATH = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 12 | DEBUG = True 13 | SECRET_KEY = '' 14 | 15 | WECHAT_APPID = os.getenv("WECHAT_APPID") 16 | WECHAT_SECRET = os.getenv("WECHAT_SECRET") 17 | WECHAT_SESSION_PREFIX = 'wechat_' 18 | WECHAT_TOKEN = 'wx123' 19 | WECHAT_TYPE = 0 20 | WECHAT_TIMEOUT = 1 21 | WECHAT_AUTO_RETRY = True 22 | 23 | OPENAI_KEY = os.getenv("OPENAI_KEY") 24 | 25 | REDIS_PASSWORD = urllib.parse.quote(os.getenv("REDIS_PWD") or "") 26 | REDIS_URL = 'redis://:%s@127.0.0.1:6379' % REDIS_PASSWORD 27 | -------------------------------------------------------------------------------- /application/config/development.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import urllib.parse 4 | 5 | from .default import BaseConfig 6 | 7 | 8 | class DevelopmentConfig(BaseConfig): 9 | ENV = "development" 10 | LOG_LEVEL = "DEBUG" 11 | SQLALCHEMY_ECHO = False # 打印sql 12 | 13 | PROFILE = True 14 | SQLALCHEMY_RECORD_QUERIES = True 15 | DATABASE_QUERY_TIMEOUT = 0.5 16 | -------------------------------------------------------------------------------- /application/config/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from application.config.development import DevelopmentConfig 5 | 6 | 7 | 8 | class LocalConfig(DevelopmentConfig): 9 | ENV = "local" 10 | DEBUG = True 11 | SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") 12 | LOG_LEVEL = "DEBUG" 13 | SQLALCHEMY_ECHO = False # 打印sql 14 | 15 | 16 | -------------------------------------------------------------------------------- /application/config/production.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import urllib.parse 4 | 5 | from .default import BaseConfig 6 | 7 | class ProductionConfig(BaseConfig): 8 | ENV = "production" 9 | LOG_LEVEL = "INFO" 10 | SQLALCHEMY_ECHO = False 11 | 12 | -------------------------------------------------------------------------------- /application/config/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from application.config.default import BaseConfig 5 | 6 | 7 | class TestingConfig(BaseConfig): 8 | TESTING = True 9 | SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' # in-memory database 10 | ENV = "testing" 11 | -------------------------------------------------------------------------------- /application/contants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import namedtuple 4 | 5 | # 默认问题码, 未知异常 6 | DEFAULT_ERROR_CODE = 500 7 | 8 | # 默认角色 9 | DEFAULT_INSTITUTION_ROLES = [ 10 | ('高级管理员', 'senior_admin'), 11 | ('普通管理员', 'normal_admin'), 12 | ] 13 | 14 | # 通用验证码 15 | UNIVERSAL_VERIFY_CODE = '0000' 16 | 17 | # 学习状态 18 | STUDY_STATUS = namedtuple("StudyStatus", "no_start, learning, finish, end")(0, 1, 2, 3) 19 | 20 | HTTP_STATUS_CODES = { 21 | 100: "Continue", 22 | 101: "Switching Protocols", 23 | 102: "Processing", 24 | 103: "Early Hints", # see RFC 8297 25 | 200: "OK", 26 | 201: "Created", 27 | 202: "Accepted", 28 | 203: "Non Authoritative Information", 29 | 204: "No Content", 30 | 205: "Reset Content", 31 | 206: "Partial Content", 32 | 207: "Multi Status", 33 | 208: "Already Reported", # see RFC 5842 34 | 226: "IM Used", # see RFC 3229 35 | 300: "Multiple Choices", 36 | 301: "Moved Permanently", 37 | 302: "Found", 38 | 303: "See Other", 39 | 304: "Not Modified", 40 | 305: "Use Proxy", 41 | 306: "Switch Proxy", # unused 42 | 307: "Temporary Redirect", 43 | 308: "Permanent Redirect", 44 | 400: "Bad Request", 45 | 401: "Unauthorized", 46 | 402: "Payment Required", # unused 47 | 403: "Forbidden", 48 | 404: "Not Found", 49 | 405: "Method Not Allowed", 50 | 406: "Not Acceptable", 51 | 407: "Proxy Authentication Required", 52 | 408: "Request Timeout", 53 | 409: "Conflict", 54 | 410: "Gone", 55 | 411: "Length Required", 56 | 412: "Precondition Failed", 57 | 413: "Request Entity Too Large", 58 | 414: "Request URI Too Long", 59 | 415: "Unsupported Media Type", 60 | 416: "Requested Range Not Satisfiable", 61 | 417: "Expectation Failed", 62 | 418: "I'm a teapot", # see RFC 2324 63 | 421: "Misdirected Request", # see RFC 7540 64 | 422: "Unprocessable Entity", 65 | 423: "Locked", 66 | 424: "Failed Dependency", 67 | 425: "Too Early", # see RFC 8470 68 | 426: "Upgrade Required", 69 | 428: "Precondition Required", # see RFC 6585 70 | 429: "Too Many Requests", 71 | 431: "Request Header Fields Too Large", 72 | 449: "Retry With", # proprietary MS extension 73 | 451: "Unavailable For Legal Reasons", 74 | 500: "Internal Server Error", 75 | 501: "Not Implemented", 76 | 502: "Bad Gateway", 77 | 503: "Service Unavailable", 78 | 504: "Gateway Timeout", 79 | 505: "HTTP Version Not Supported", 80 | 506: "Variant Also Negotiates", # see RFC 2295 81 | 507: "Insufficient Storage", 82 | 508: "Loop Detected", # see RFC 5842 83 | 510: "Not Extended", 84 | 511: "Network Authentication Failed", # see RFC 6585 85 | } 86 | 87 | # wsapp 的回调消息状态码 88 | WSAPI_CALLBACK_STATUS_CODE = { 89 | 100: "replaced登录", 90 | 101: "链接已断开!", 91 | 200: "登录成功", 92 | 202: "已在线", 93 | -401: "密钥认证失败", 94 | 403: "当前账号被禁止使用", 95 | 96 | 2000: "对话消息=2000", # 接收到消息 97 | 2001: "已送达消息返回xml", 98 | 2004: "presence回调消息返回xml", 99 | 2005: "对方和自己已读消息回执返回xml", 100 | 101 | } 102 | -------------------------------------------------------------------------------- /application/contrib/cache.py: -------------------------------------------------------------------------------- 1 | from flask_redis import FlaskRedis 2 | 3 | 4 | class Cache(FlaskRedis): 5 | """ 6 | redis 常用操作 http://www.imooc.com/wiki/pythonlesson2/pyredis.html 7 | """ 8 | _ROOM_USERS_KEY = "ROOM_{}_USERS_KEY" # 房间用户列表KEY 9 | _ROOM_KEY = "ROOM_{}_KEY" # 房间用户列表KEY 10 | _HEATBEAT_ROOM_USER_KEY = "HB_ROOM_{}_USER_{}" # 用户心跳记录 11 | 12 | CHAT_USER_KEY = "USER_{}_KEY" 13 | 14 | def push(self, openid, data): 15 | self.rpush(self.CHAT_USER_KEY.format(openid), data) 16 | 17 | def pop(self, openid): 18 | return self.rpop(self.CHAT_USER_KEY.format(openid)) 19 | -------------------------------------------------------------------------------- /application/contrib/openai_api.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import time 3 | from typing import List 4 | 5 | import openai 6 | 7 | 8 | class OpenAI: 9 | 10 | def __init__(self, app=None): 11 | self.openai = openai 12 | self.app = app 13 | if self.app: 14 | self.init_app(app) 15 | pass 16 | 17 | def init_app(self, app): 18 | self.openai.api_key = app.config.get("OPENAI_KEY") 19 | pass 20 | 21 | def answer(self, question: str, context: List[dict] = None): 22 | message = {"role": "user", 23 | "content": question} 24 | if context: 25 | messages = [] 26 | for m in context: 27 | t = m.pop("t", 0) 28 | if t > time.time() - 5 * 60: 29 | messages.append(m) 30 | messages.append(message) 31 | else: 32 | messages = [message] 33 | 34 | response = self.openai.ChatCompletion.create(model="gpt-3.5-turbo", 35 | messages=messages, 36 | temperature=0) 37 | # print(response) 38 | return response.get("choices")[0].get("message").get("content") 39 | -------------------------------------------------------------------------------- /application/dto/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .wechat import * 3 | -------------------------------------------------------------------------------- /application/dto/base.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel as Base, Field 2 | 3 | 4 | class BaseModel(Base): 5 | class Config: 6 | orm_mode = True 7 | use_enum_values = True 8 | 9 | 10 | class QueryFilterDto(BaseModel): 11 | """ 12 | 查询列表 13 | """ 14 | page: int = 1 15 | size: int = 20 16 | keyword: str = Field(title="搜索关键字", default=None) 17 | 18 | 19 | class ResponseSuccessDto(BaseModel): 20 | code: int = 200 21 | msg: str = "success" 22 | -------------------------------------------------------------------------------- /application/dto/wechat.py: -------------------------------------------------------------------------------- 1 | from .base import BaseModel 2 | 3 | 4 | class SignatureDto(BaseModel): 5 | signature: str 6 | echostr: str 7 | timestamp: str 8 | nonce: str 9 | -------------------------------------------------------------------------------- /application/enum_field.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, Enum 2 | 3 | 4 | class VisibleRange(Enum): 5 | owner = "owner" 6 | part = "part" 7 | all = "all" 8 | 9 | class Config: 10 | use_enum_values = True 11 | 12 | 13 | class QuestionType(IntEnum): 14 | SINGLE = 1 15 | MULTIPLE = 2 16 | BOOLEAN = 3 17 | 18 | 19 | class FileFormat(Enum): 20 | VIDEO = "video" 21 | LIVE = "live" 22 | 23 | class Config: 24 | use_enum_values = True 25 | 26 | 27 | class CourseAction(Enum): 28 | DELETE = "delete" 29 | LAUNCH = "launch" 30 | UNLAUNCH = "unlaunch" 31 | 32 | 33 | class MaterialType(Enum): 34 | """ 35 | 素材类型 36 | """ 37 | IMAGE = 'image' 38 | VIDEO = 'video' 39 | AUDIO = 'audio' 40 | 41 | 42 | class CommentType(Enum): 43 | IMAGE = 'image' 44 | VIDEO = 'video' 45 | TEXT = 'text' 46 | 47 | 48 | class TransferStatus(Enum): 49 | """ 50 | 转码状态 51 | """ 52 | FINISH = 'FINISH' 53 | PROCESSING = 'PROCESSING' 54 | 55 | 56 | class CourseQueryType(Enum): 57 | NEWEST = "newest" 58 | BROWSE = "browse" 59 | 60 | 61 | class StudyStatus(IntEnum): 62 | NO_START = 0 # 未开始学习 63 | LEARNING = 1 # 学习中 64 | FINISH = 2 # 已学完(考试还未通过) 65 | END = 3 # 已学完并且考试通过 (或者没有考试) 66 | 67 | 68 | class PageType(Enum): 69 | MICRO_PAGE = 'micro_page' 70 | MESSAGE = 'message' 71 | MINE = 'mine' 72 | CUSTOM = 'custom' 73 | MAIN = 'main' 74 | ME = 'me' 75 | VIDEO = 'video' # 视频详情 76 | TEXT = 'text' # 图文详情 77 | 78 | 79 | class NotificationCategory(IntEnum): 80 | SYSTEM = 2 # 系统消息 81 | NEWS = 1 # 资讯消息 82 | 83 | 84 | class StatDimension(Enum): 85 | DAY = 'day' 86 | WEEK = 'week' 87 | MONTH = 'month' 88 | YEAR = 'year' 89 | 90 | 91 | class TagComputeState(IntEnum): 92 | """ 93 | 标签计算状态 94 | """ 95 | FINISH = 2 96 | COMPUTING = 1 97 | 98 | 99 | class TagType(IntEnum): 100 | """ 101 | 标签类型 102 | """ 103 | MANUAL = 1 # 手动 104 | AUTO = 2 # 自动 105 | 106 | 107 | class ActivityShowType(Enum): 108 | ONCE = 'once' # 显示一次 109 | REPEAT = 'repeat' # 重复显示 110 | 111 | 112 | class ActivityStatus(Enum): 113 | NO_START = "no_start" 114 | ONGOING = 'ongoing' 115 | ENDED = 'ended' 116 | DISABLED = 'disabled' 117 | 118 | 119 | class RankStatus(Enum): 120 | NO_START = "no_start" 121 | ONGOING = 'ongoing' 122 | ENDED = 'ended' 123 | DISABLED = 'disabled' 124 | 125 | 126 | class RankShareChannel(IntEnum): 127 | POSTER = 1 128 | WECHAT = 2 129 | LINK = 3 130 | 131 | 132 | class LiveStatus(Enum): 133 | PREPARE = 'prepare' # 预告 134 | LIVE = "live" # 直播中 135 | REPLAY = 'replay' # 回放中 136 | 137 | 138 | class PlayDirection(Enum): 139 | HORIZONTAL = "horizontal" 140 | VERTICAL = 'vertical' 141 | 142 | 143 | class CourseMenuItemCode(Enum): 144 | DESCRIPTION = "introduce" 145 | LIST = "list" 146 | chat = "chat" 147 | 148 | 149 | class CopyrightStat(IntEnum): 150 | NO_ORIGINAL = 100 151 | ORIGINAL = 11 152 | 153 | 154 | class CourseKind(Enum): 155 | TEXT = "text" 156 | VIDEO = "video" 157 | 158 | 159 | class ResourceKind(Enum): 160 | IMAGE = "image" 161 | COURSE = "course" 162 | -------------------------------------------------------------------------------- /application/errors.py: -------------------------------------------------------------------------------- 1 | ERROR_CODE = { 2 | 3 | 200: "success", 4 | } 5 | class ApiError(Exception): 6 | """所有服务异常的基类""" 7 | code = 500 8 | http_code = 400 9 | data = None 10 | error_info = "" 11 | msg = "" 12 | 13 | def __init__(self, msg=None, code=None, data=None, error_info=None, http_code=None): 14 | if msg: 15 | self.msg = msg 16 | if code: 17 | self.code = code 18 | 19 | if data: 20 | self.data = data 21 | 22 | if http_code: 23 | self.http_code = http_code 24 | 25 | self.error_info = error_info 26 | 27 | 28 | class RequestDataError(ApiError): 29 | code = 1002 30 | http_code = 400 31 | msg = "请求参数错误" 32 | 33 | 34 | class AuthForbiddenError(ApiError): 35 | """鉴权失败异常 用户已禁用""" 36 | code = 2042 37 | http_code = 401 38 | msg = "您的帐号暂无权限,请联系管理员~" 39 | 40 | 41 | class MaterialError(ApiError): 42 | code = 6200 43 | http_code = 400 44 | 45 | 46 | class UserProfileError(ApiError): 47 | code = 6400 48 | http_code = 400 49 | 50 | 51 | class WechatError(ApiError): 52 | code = 6500 53 | http_code = 400 54 | 55 | 56 | class WxTemplateError(ApiError): 57 | code = 6600 58 | http_code = 400 59 | -------------------------------------------------------------------------------- /application/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_siwadoc import SiwaDoc 2 | 3 | from application.contrib.cache import Cache 4 | from application.contrib.openai_api import OpenAI 5 | 6 | siwa = SiwaDoc(title="API") 7 | openai_api = OpenAI() 8 | cache = Cache() 9 | -------------------------------------------------------------------------------- /application/middlewares.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class WSGICopyBodyMiddleware(object): 5 | """ 6 | 没有找到可靠的方法找到原始的请求体, 参考了stackoverflow上面一个回答 7 | https://stackoverflow.com/questions/10999990/get-raw-post-body-in-python-flask-regardless-of-content-type-header 8 | 代码基本相同, 只是兼容了python3 9 | """ 10 | def __init__(self, application): 11 | self.application = application 12 | 13 | def __call__(self, environ, start_response): 14 | 15 | from io import BytesIO 16 | length = environ.get('CONTENT_LENGTH', '0') 17 | length = 0 if length == '' else int(length) 18 | 19 | body = environ['wsgi.input'].read(length) 20 | environ['body_copy'] = body 21 | environ['wsgi.input'] = BytesIO(body) 22 | 23 | # Call the wrapped application 24 | app_iter = self.application(environ, 25 | self._sr_callback(start_response)) 26 | 27 | # Return modified response 28 | return app_iter 29 | 30 | def _sr_callback(self, start_response): 31 | def callback(status, headers, exc_info=None): 32 | 33 | # Call upstream start_response 34 | start_response(status, headers, exc_info) 35 | return callback 36 | 37 | -------------------------------------------------------------------------------- /application/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/application/models/__init__.py -------------------------------------------------------------------------------- /application/models/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from datetime import datetime 4 | 5 | import inflection 6 | from sqlalchemy import Column, Boolean 7 | from sqlalchemy.ext import mutable 8 | from sqlalchemy.ext.declarative import declared_attr 9 | from sqlalchemy.orm.query import Query 10 | from sqlalchemy.sql import ClauseElement 11 | 12 | from application.extensions import db 13 | from application.utils import random_utils 14 | 15 | 16 | class DatetimeMixin: 17 | __abstract__ = True 18 | """创建时间和修改时间mixin, 参考 19 | """ 20 | id = db.Column(db.String(24), primary_key=True, comment="主键", name='id', default=random_utils.objectid) 21 | created_at = db.Column(db.DateTime, default=datetime.now, nullable=False, comment="创建时间") 22 | updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="最后更新时间") 23 | deleted_at = db.Column(db.DateTime, comment='删除时间', nullable=True) 24 | 25 | 26 | class JsonArray(db.TypeDecorator): 27 | """Enables JSON storage by encoding and decoding on the fly.""" 28 | impl = db.Text 29 | 30 | @property 31 | def python_type(self): 32 | return list 33 | 34 | def process_bind_param(self, value, dialect): 35 | if value is None: 36 | return '[]' 37 | else: 38 | return json.dumps(value) 39 | 40 | def process_result_value(self, value, dialect): 41 | if value is None: 42 | return [] 43 | else: 44 | return json.loads(value) 45 | 46 | 47 | mutable.MutableList.associate_with(JsonArray) 48 | 49 | 50 | class JsonDict(db.TypeDecorator): 51 | """Enables JSON storage by encoding and decoding on the fly.""" 52 | impl = db.Text 53 | 54 | @property 55 | def python_type(self): 56 | return dict 57 | 58 | def process_bind_param(self, value, dialect): 59 | if value is not None: 60 | return json.dumps(value) 61 | 62 | def process_result_value(self, value, dialect): 63 | if value is not None: 64 | return json.loads(value) 65 | 66 | 67 | mutable.MutableDict.associate_with(JsonDict) 68 | 69 | 70 | class BaseModel(DatetimeMixin, db.Model): 71 | """ 72 | :type: sqlalchemy.orm.Session 73 | """ 74 | is_enable = Column(Boolean, default=True, nullable=False, comment="是否可用") 75 | is_deleted = Column(Boolean, default=False, comment="是否已删除") # 原来的用户状态:'-1删除,0禁用,1启用 76 | __abstract__ = True 77 | 78 | default_fields = [] 79 | 80 | def __str__(self): 81 | return f"<{self.__class__.__name__} {self.id}>" 82 | 83 | @declared_attr 84 | def __tablename__(cls): 85 | return inflection.underscore(cls.__name__) 86 | 87 | def update(self, **kwargs): 88 | keys = self.__table__.columns.keys() 89 | for key, value in kwargs.items(): 90 | if key in keys: 91 | setattr(self, key, value) 92 | return self 93 | 94 | def can_edit_visible(self, user, category) -> bool: 95 | """ 96 | 判断user是否可以编辑 97 | 只有创建者、超级管理员、机构创建者才有编辑权限 98 | :param user: InstitutionUser 99 | :param category ResourceCategory 100 | :return: bool 101 | """ 102 | result = any([user.id == self.created_by_id, 'creator' in user.role_codes]) 103 | return result 104 | 105 | def to_dict(self, show=None, hide=None, path=None, show_all=None, list_item=False): 106 | """ Return a dictionary representation of this model. 107 | """ 108 | 109 | if not show: 110 | show = [] 111 | if not hide: 112 | hide = [] 113 | hidden = [] 114 | if hasattr(self, 'hidden_fields'): 115 | hidden = self.hidden_fields 116 | default = [] 117 | if hasattr(self, 'default_fields'): 118 | default = self.default_fields 119 | if self.default_fields == 'all' and not show: 120 | default = self.__table__.columns.keys() \ 121 | + self.__table__.columns.keys() \ 122 | + dir(self) 123 | ret_data = {} 124 | 125 | if not path: 126 | path = self.__tablename__.lower() 127 | 128 | def prepend_path(item): 129 | item = item.lower() 130 | if item.split('.', 1)[0] == path: 131 | return item 132 | if len(item) == 0: 133 | return item 134 | if item[0] != '.': 135 | item = '.%s' % item 136 | item = '%s%s' % (path, item) 137 | return item 138 | 139 | show[:] = [prepend_path(x) for x in show] 140 | hide[:] = [prepend_path(x) for x in hide] 141 | 142 | columns = self.__table__.columns.keys() 143 | relationships = self.__mapper__.relationships.keys() 144 | properties = dir(self) 145 | 146 | for key in columns: 147 | check = '%s.%s' % (path, key) 148 | if check in hide or key in hidden: 149 | continue 150 | if show_all or check in show or key in default: 151 | ret_data[key] = getattr(self, key) 152 | 153 | for key in relationships: 154 | check = '%s.%s' % (path, key) 155 | if check in hide or key in hidden: 156 | continue 157 | if show_all or check in show or key in default: 158 | if not list_item: 159 | hide.append(check) 160 | is_list = self.__mapper__.relationships[key].uselist 161 | if is_list: 162 | ret_data[key] = [] 163 | for item in getattr(self, key): 164 | ret_data[key].append(item.to_dict( 165 | show=[], 166 | hide=hide, 167 | path=('%s.%s' % (path, key.lower())), 168 | show_all=show_all, 169 | list_item=True 170 | )) 171 | else: 172 | if self.__mapper__.relationships[key].query_class is not None: 173 | if getattr(self, key): 174 | ret_data[key] = getattr(self, key).to_dict( 175 | show=[], 176 | hide=hide, 177 | path=('%s.%s' % (path, key.lower())), 178 | show_all=show_all, 179 | ) 180 | else: 181 | ret_data[key] = None 182 | else: 183 | ret_data[key] = getattr(self, key) 184 | 185 | for key in list(set(properties) - set(columns) - set(relationships)): 186 | if key.startswith('_'): 187 | continue 188 | check = '%s.%s' % (path, key) 189 | if check in hide or key in hidden: 190 | continue 191 | 192 | if show_all or check in show or key in default: 193 | val = getattr(self, key) 194 | # 过滤掉非property的方法 195 | if not isinstance(getattr(type(self), key), property): 196 | continue 197 | 198 | if callable(val): 199 | val = val() 200 | try: 201 | ret_data[key] = val 202 | except: 203 | pass 204 | 205 | return ret_data 206 | 207 | @classmethod 208 | def order_fields(cls, order_bys: str): 209 | """ 210 | 排序处理 211 | param:order_bys, 用逗号分隔的排序字段, 字段如果以“-”开头则为升序 212 | """ 213 | if not order_bys: 214 | return 215 | order_bys = order_bys.split(",") 216 | orders = [] 217 | for i in order_bys: 218 | field = i[1:] if i.startswith("-") else i 219 | if hasattr(cls, field): 220 | if i.startswith("-"): 221 | orders.append(getattr(cls, field)) 222 | else: 223 | # 降序 224 | orders.append(getattr(cls, field).desc()) 225 | return orders 226 | 227 | @classmethod 228 | def build_query(cls, query: Query = None, **kwargs): 229 | """ 230 | 根据指定参数构建过滤条件 231 | """ 232 | if query is None: 233 | query = cls.query.filter_by(**kwargs) 234 | else: 235 | query = query.filter_by(**kwargs) 236 | return query 237 | 238 | @classmethod 239 | def get_or_create(cls, defaults=None, **kwargs): 240 | instance = cls.query.filter_by(**kwargs).first() 241 | if instance: 242 | return instance, False 243 | else: 244 | params = dict((k, v) for k, v in kwargs.items() if not isinstance(v, ClauseElement)) 245 | params.update(defaults or {}) 246 | instance = cls(**params) 247 | db.session.add(instance) 248 | db.session.flush() 249 | return instance, True 250 | 251 | @classmethod 252 | def upsert(cls, filter_data: dict, update_data: dict): 253 | """ 254 | 插入或更新 255 | """ 256 | instance = cls.query.filter_by(**filter_data).first() 257 | if not instance: 258 | instance = cls(**filter_data) 259 | instance.update(**update_data) 260 | db.session.add(instance) 261 | return instance 262 | 263 | @classmethod 264 | def get_by_id(cls, _id: str): 265 | return cls.query.get(_id) 266 | 267 | 268 | class CategoryModel(BaseModel): 269 | """ 270 | 分类基类 271 | """ 272 | __abstract__ = True 273 | name = db.Column(db.String(64)) 274 | description = db.Column(db.Text, nullable=True) 275 | parent_id = db.Column(db.Integer, default=0, nullable=True) 276 | key = db.Column(db.String(100), default="") # 第0层 直接用“”表示,第一层用“主键id-”表示 277 | position = db.Column(db.Integer, default=0) # 同层级的子节点的先后顺序 278 | is_deleted = db.Column(db.Boolean, nullable=True, default=False, comment="是否删除") 279 | 280 | @declared_attr 281 | def institution_id(cls): 282 | return db.Column(db.String(24), db.ForeignKey('institution.id', use_alter=True), comment="机构id") 283 | 284 | @declared_attr 285 | def created_by_id(cls): 286 | return db.Column(db.String(24), db.ForeignKey('institution_user.id'), comment="创建这个课程的员工", nullable=True) 287 | 288 | default_fields = ['id', 'name', "description", "created_at"] 289 | -------------------------------------------------------------------------------- /application/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/application/services/__init__.py -------------------------------------------------------------------------------- /application/services/openai_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from application.extensions import openai_api, cache 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | CHATDATA = {} # 保存历史记录 9 | 10 | 11 | def set_answer(openid, question): 12 | logger.info("获取question:%s" % question) 13 | answer = openai_api.answer(question, context=CHATDATA.get(openid)).strip() 14 | CHATDATA.setdefault(openid, []).append({"role": "user", "content": question, 't': time.time()}) 15 | CHATDATA[openid].append({"role": "assistant", "content": answer, 't': time.time()}) 16 | CHATDATA[openid] = CHATDATA[openid][-5:] 17 | logger.info("获取answer:%s" % answer) 18 | cache.push(openid, answer) 19 | -------------------------------------------------------------------------------- /application/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | pass 4 | -------------------------------------------------------------------------------- /application/templates/company.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
{{title}}
23 |
24 | 请输入公司介绍的文本段落 25 |
26 | 28 |
29 | 30 |
31 |
32 |

{{answer}}

33 |
34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /application/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import logging 4 | import re 5 | from datetime import datetime 6 | 7 | from flask import current_app, Request 8 | 9 | from .random_utils import random_num_str, random_str, random_order_number, random_base16_num_str 10 | from .response_utils import error 11 | from .response_utils import success 12 | from .time_utils import timestamp_to_datetime 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | def make_hmac(key, data): 17 | """ 18 | HMAC-SHA256签名算法 19 | """ 20 | 21 | if isinstance(key, str): 22 | key = bytes(key, encoding="utf-8") 23 | if isinstance(data, str): 24 | data = bytes(data, encoding="utf-8") 25 | hash = hmac.new(key, data, digestmod=hashlib.sha256) 26 | result = hash.digest() 27 | return result 28 | 29 | 30 | # 用来做表单数据自动转换用的函数 31 | coerceDate = lambda d: datetime.strptime(d, "%Y-%m-%dT%H:%M:%S.%f") 32 | 33 | 34 | def make_md5(data): 35 | md = hashlib.md5() 36 | md.update(data.encode("utf-8")) 37 | result = md.hexdigest() 38 | return result 39 | 40 | 41 | def sub_dict(d: dict, included_keys: list): 42 | return {k: d[k] for k in included_keys if k in d} 43 | 44 | 45 | def list_is_equals(a_list, b_list): 46 | """ 47 | 判断两个列表中是否含有相同的元素 48 | """ 49 | return set(a_list) == set(b_list) 50 | 51 | 52 | def get_institution_code_by_hostname(host: str): 53 | """ 54 | 从host中提取出机构code 55 | :param host: xinylzb40765841-h5.youbaokeji.cn 56 | """ 57 | match = re.search(r"(live|xinylzb)(\w*)-h5\.*?", host) 58 | if match: 59 | institution_code = match.group(2) 60 | else: 61 | institution_code = current_app.config.get("DEFAULT_INSTITUTION_CODE") 62 | return institution_code 63 | 64 | 65 | def get_institution_code_by_request(request: Request): 66 | """ 67 | 从request的请求头名叫hostx取出前端传的访问域名后解析出code 68 | :param host: xinylzb40765841-h5.youbaokeji.cn 69 | """ 70 | hostx = request.headers.get("hostx") 71 | logger.info("hosts:%s", hostx) 72 | if not hostx: 73 | hostx = request.headers.get("referer") 74 | logger.info("referer:%s", hostx) 75 | return get_institution_code_by_hostname(hostx) 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /application/utils/aes_cipher.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import random 4 | 5 | from Crypto import Random 6 | from Crypto.Cipher import AES 7 | 8 | 9 | class AESCipher: 10 | 11 | def __init__(self, key): 12 | self.bs = AES.block_size 13 | self.key = key.encode('utf-8') 14 | 15 | # def encrypt(self, raw): 16 | # raw = self._pad(raw) 17 | # iv = Random.new().read(AES.block_size) 18 | # cipher = AES.new(self.key, AES.MODE_CBC, iv) 19 | # return base64.b64encode(iv + cipher.encrypt(raw.encode())) 20 | 21 | def encrypt(self, plain_text, iv): 22 | cipher = AES.new(self.key, AES.MODE_CBC, iv.encode('utf-8')) 23 | content_padding = self.aes_pkcs5_padding(plain_text, self.bs) 24 | encrypt_bytes = cipher.encrypt(content_padding.encode('utf-8')) 25 | return encrypt_bytes 26 | 27 | def aes_pkcs5_padding(self, cipher_text, block_size): 28 | padding_size = len(cipher_text) if (len(cipher_text) == len( 29 | cipher_text.encode('utf-8'))) else len(cipher_text.encode('utf-8')) 30 | padding = block_size - padding_size % block_size 31 | if padding < 0: 32 | return None 33 | padding_text = chr(padding) * padding 34 | return cipher_text + padding_text 35 | 36 | 37 | 38 | # 39 | # def decrypt(self, enc): 40 | # enc = base64.b64decode(enc) 41 | # iv = enc[:AES.block_size] 42 | # cipher = AES.new(self.key, AES.MODE_CBC, iv) 43 | # return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') 44 | 45 | # def _pad(self, s): 46 | # return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) 47 | # 48 | # @staticmethod 49 | # def _unpad(s): 50 | # return s[:-ord(s[len(s) - 1:])] 51 | -------------------------------------------------------------------------------- /application/utils/base64_utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import random 3 | 4 | import requests 5 | 6 | 7 | def base64_from_url(url): 8 | """ 9 | 获取url资源的base64编码数据 10 | """ 11 | return base64.b64encode(requests.get(url).content).decode() 12 | 13 | 14 | def generate_base64_image(): 15 | url = random.choice(avatars) 16 | return base64_from_url(url) 17 | 18 | 19 | avatars = ["https://pic1.zhimg.com/50/v2-d7a1597151e7c8253e6706105ab32f42_s.jpg?source=57bbeac9", 20 | "https://pica.zhimg.com/v2-553c5548645d5669566782726f3bd89b_s.jpg?source=06d4cd63", 21 | "https://pic2.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 22 | "https://pic1.zhimg.com/v2-e5136bce6745bd60511c79838b6b82ce_s.jpg?source=06d4cd63", 23 | "https://pic1.zhimg.com/v2-49ec3158481d679fc8dbd5961b43d62d_s.jpg?source=06d4cd63", 24 | "https://pic1.zhimg.com/v2-d7a1597151e7c8253e6706105ab32f42_s.jpg?source=06d4cd63", 25 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 26 | "https://pic2.zhimg.com/v2-b9bbc2fd87e5b1ae9594f7a7de6dadce_s.jpg?source=06d4cd63", 27 | "https://pic2.zhimg.com/v2-6503078ad512550a71d152f3193d00b9_s.jpg?source=06d4cd63", 28 | "https://pic2.zhimg.com/a6034b9707a43242de74e7fcd32be3e5_s.jpg?source=06d4cd63", 29 | "https://pic3.zhimg.com/v2-d7a1597151e7c8253e6706105ab32f42_s.jpg?source=06d4cd63", 30 | "https://pic4.zhimg.com/e93fdd1be4c6bac4d5deb1c502877963_s.jpg?source=06d4cd63", 31 | "https://pic4.zhimg.com/v2-2b9d611e0c236f6048e17171786428f4_s.jpg?source=06d4cd63", 32 | "https://pic2.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 33 | "https://pica.zhimg.com/v2-e4e77097ab402a95bec309853f592138_s.jpg?source=06d4cd63", 34 | "https://pic1.zhimg.com/v2-e65c582f61d3c1424624cccfb287eec9_s.jpg?source=06d4cd63", 35 | "https://pic2.zhimg.com/v2-3ed4be5b35de984008fd900c50e3110a_s.jpg?source=06d4cd63", 36 | "https://pica.zhimg.com/v2-a298985ec5b9f11b62fe6d246ee132f7_s.jpg?source=06d4cd63", 37 | "https://pic1.zhimg.com/v2-ade36ff4f29d69811fbf15dab3bc91cc_s.jpg?source=06d4cd63", 38 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 39 | "https://pic1.zhimg.com/v2-d7a1597151e7c8253e6706105ab32f42_s.jpg?source=06d4cd63", 40 | "https://pica.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 41 | "https://pic2.zhimg.com/f9d47cc2c6fb8d39aebea78acdbb6245_s.jpg?source=06d4cd63", 42 | "https://pic3.zhimg.com/v2-798d13a8469caf7d31371268b69a9ce2_s.jpg?source=06d4cd63", 43 | "https://pic2.zhimg.com/50/v2-f65da433d7f77abf5428cfec69f9690b_s.jpg?source=57bbeac9", 44 | "https://pic1.zhimg.com/50/v2-ff632f212e0809d6b98c60cf9fdf8684_s.jpg?source=57bbeac9", 45 | "https://pic4.zhimg.com/50/v2-f3fdee398b29d55ef43510685ee1acbb_s.jpg?source=57bbeac9", 46 | "https://pic3.zhimg.com/50/v2-361dee59289f981001386d43dfebe589_s.jpg?source=57bbeac9", 47 | "https://pic1.zhimg.com/v2-39691031cd1416bbed5897d393da0bfd_400x224.jpg?source=7e7ef6e2", 48 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 49 | "https://pica.zhimg.com/v2-46e42cfa12ee36e373a6c6d1a6dc5317_s.jpg?source=06d4cd63", 50 | "https://pic3.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 51 | "https://pic1.zhimg.com/v2-361dee59289f981001386d43dfebe589_s.jpg?source=06d4cd63", 52 | "https://pic2.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 53 | "https://pic3.zhimg.com/v2-4a61aa3b21c222a0f2c43935aea7d29b_s.jpg?source=06d4cd63", 54 | "https://pic3.zhimg.com/v2-361dee59289f981001386d43dfebe589_s.jpg?source=06d4cd63", 55 | "https://pica.zhimg.com/v2-ae7ab013273da604b6b1fcb6061b7f5b_s.jpg?source=06d4cd63", 56 | "https://pic2.zhimg.com/v2-361dee59289f981001386d43dfebe589_s.jpg?source=06d4cd63", 57 | "https://pic1.zhimg.com/v2-361dee59289f981001386d43dfebe589_s.jpg?source=06d4cd63", 58 | "https://pic3.zhimg.com/v2-361dee59289f981001386d43dfebe589_s.jpg?source=06d4cd63", 59 | "https://pic1.zhimg.com/v2-ae7ab013273da604b6b1fcb6061b7f5b_s.jpg?source=06d4cd63", 60 | "https://pic1.zhimg.com/v2-bf1d34a4a92d6a64349d66092078ae19_s.jpg?source=06d4cd63", 61 | "https://pic3.zhimg.com/v2-22d75dd6027090cff1e9cbd3a56018ad_s.jpg?source=06d4cd63", 62 | "https://pic1.zhimg.com/v2-8acb23f66ccb39fb224b96c653fdb580_s.jpg?source=06d4cd63", 63 | "https://pic1.zhimg.com/v2-6127a23f4c1b4be4a8ba419d2be77e32_s.jpg?source=06d4cd63", 64 | "https://pic2.zhimg.com/v2-e809253fc6597f75a5599e60b5062812_s.jpg?source=06d4cd63", 65 | "https://pic2.zhimg.com/v2-361dee59289f981001386d43dfebe589_s.jpg?source=06d4cd63", 66 | "https://pic1.zhimg.com/v2-8b7189410db9a4a28e224804f939ccda_s.jpg?source=06d4cd63", 67 | "https://pic2.zhimg.com/v2-2bdd1f2798409dbda029191821a9c6bd_s.jpg?source=06d4cd63", 68 | "https://pica.zhimg.com/v2-8ef1977db29c71b8369771f4c053ff4a_s.jpg?source=06d4cd63", 69 | "https://pic2.zhimg.com/v2-1f35e6795278dac0ba2fc7ca3fba9d51_s.jpg?source=06d4cd63", 70 | "https://pic3.zhimg.com/v2-019e2ecb945dbc3c0d463f9ca5732fd3_s.jpg?source=06d4cd63", 71 | "https://pica.zhimg.com/v2-f9200cf8cd331c7dd0b022e730b159a7_s.jpg?source=06d4cd63", 72 | "https://pic3.zhimg.com/v2-f9d1c83c94c169367a5b719e6f208d87_s.jpg?source=06d4cd63", 73 | "https://pic2.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 74 | "https://pic3.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 75 | "https://pic2.zhimg.com/v2-a0da7d712158a3193030e02719ba2839_s.jpg?source=06d4cd63", 76 | "https://pica.zhimg.com/v2-92c9562e19d5d15adaaa059e20573bd5_s.jpg?source=06d4cd63", 77 | "https://pic1.zhimg.com/v2-f79e55c9e428adc496cfb0448a913228_s.jpg?source=06d4cd63", 78 | "https://pic1.zhimg.com/50/v2-fb115773c94d8281fc0e0717645f5506_s.jpg?source=57bbeac9", 79 | "https://pic1.zhimg.com/50/v2-303592120fc8c28595574da37998b29d_s.jpg?source=57bbeac9", 80 | "https://pic3.zhimg.com/v2-ea2cc2b8468804ec95dc0eea3890232f_400x224.jpg?source=7e7ef6e2", 81 | "https://pic1.zhimg.com/v2-756e0977f6f537d993b44f379176c3e4_s.jpg?source=06d4cd63", 82 | "https://pic2.zhimg.com/v2-c59b7eb3135b62ff5155315a502136e6_s.jpg?source=06d4cd63", 83 | "https://pic1.zhimg.com/8d32de510fc48a7398b0e55a2632f30c_s.jpg?source=06d4cd63", 84 | "https://pic1.zhimg.com/v2-fc7ce44b04c89bb8695e291ec7e9a9e0_s.jpg?source=06d4cd63", 85 | "https://pic1.zhimg.com/v2-093bc0b9ff9b641c26ab8843e4aac24d_s.jpg?source=06d4cd63", 86 | "https://pica.zhimg.com/v2-4dcacd6df84847096599092fcf5b7d88_s.jpg?source=06d4cd63", 87 | "https://pic2.zhimg.com/v2-274badea93c5509cb4722846f3006e9b_s.jpg?source=06d4cd63", 88 | "https://pica.zhimg.com/v2-4046ab672f3d52041b789d0040da4cac_s.jpg?source=06d4cd63", 89 | "https://pic3.zhimg.com/v2-59de6244191ffa34115b0a70197b38d7_s.jpg?source=06d4cd63", 90 | "https://pic3.zhimg.com/v2-cf48050b86b0e89ddcaf782efcf2bf98_s.jpg?source=06d4cd63", 91 | "https://pic2.zhimg.com/v2-69d49c10972577b521f3b88a8b972e08_s.jpg?source=06d4cd63", 92 | "https://pic2.zhimg.com/v2-1bb0e7713c0da8215dfc904cd80321b3_s.jpg?source=06d4cd63", 93 | "https://pica.zhimg.com/0d2940ce5d848d6dc53b780a88dc7119_s.jpg?source=06d4cd63", 94 | "https://pic1.zhimg.com/v2-fc59231629a2d4f07f3df9888e3cf85b_s.jpg?source=06d4cd63", 95 | "https://pic2.zhimg.com/v2-8707934fff107659340817c436d20953_s.jpg?source=06d4cd63", 96 | "https://pica.zhimg.com/v2-8e8b2b703abcf339117f0622d9ac413d_s.jpg?source=06d4cd63", 97 | "https://pica.zhimg.com/v2-a4b248e6917cf3f298ba357262b6012b_s.jpg?source=06d4cd63", 98 | "https://pic3.zhimg.com/50/v2-91cc08223b0f1f099051c30f58d6858e_s.jpg?source=57bbeac9", 99 | "https://pica.zhimg.com/50/v2-0c4624c5800f6f51d2ff2563e6de4950_s.jpg?source=57bbeac9", 100 | "https://pic3.zhimg.com/50/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=57bbeac9", 101 | "https://pica.zhimg.com/v2-ef73c3b8e9096c6dd765c4ac14db7d64_400x224.jpg?source=7e7ef6e2", 102 | "https://pic2.zhimg.com/50/v2-4f963a63f7f2451c67a1386adf0408fa_s.jpg?source=57bbeac9", 103 | "https://pic2.zhimg.com/v2-56373c950767dcd5ece854bd912de890_400x224.jpg?source=7e7ef6e2", 104 | "https://pic3.zhimg.com/50/v2-8eb6e3c1c15276dbbc9f16ba58289da0_s.jpg?source=57bbeac9", 105 | "https://pica.zhimg.com/50/v2-e5a43b568c9d6b0d5e87c6ea07917284_s.jpg?source=57bbeac", 106 | "https://pic1.zhimg.com/v2-35dcf70ff57108ef92fc890b13e01662_s.jpg?source=06d4cd63", 107 | "https://pica.zhimg.com/v2-f38710ed32ff15807c5b7244ac9dafe0_s.jpg?source=06d4cd63", 108 | "https://pic3.zhimg.com/v2-a5fba78b493e2812a1ae4b53355574c4_s.jpg?source=06d4cd63", 109 | "https://pic3.zhimg.com/v2-385ee4ceff7ac4cbbd7337e3cc96ebce_s.jpg?source=06d4cd63", 110 | "https://pic2.zhimg.com/v2-8b32b802369f44738fd6ae65a639ab59_s.jpg?source=06d4cd63", 111 | "https://pica.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 112 | "https://pica.zhimg.com/v2-9f045987e0f40c00a72eb7292254a4cf_s.jpg?source=06d4cd63", 113 | "https://pic1.zhimg.com/v2-dc8bd7ae8513c46ccce803be2f9c9495_s.jpg?source=06d4cd63", 114 | "https://pic1.zhimg.com/v2-e6d1f7072e5978faf50583a2189c6df6_s.jpg?source=06d4cd63", 115 | "https://pic1.zhimg.com/v2-645b87814060f2e30161c10de2edf6b4_s.jpg?source=06d4cd63", 116 | "https://pica.zhimg.com/v2-9bce13d4b89da3f99c326739f5f5a226_s.jpg?source=06d4cd63", 117 | "https://pica.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 118 | "https://pic2.zhimg.com/v2-0718cc616df690bac192045ddc0545cd_s.jpg?source=06d4cd63", 119 | "https://pic1.zhimg.com/v2-26409271b03cbc37ca03fbfe13017c35_s.jpg?source=06d4cd63", 120 | "https://pic3.zhimg.com/v2-6347def85e10d2b3e53f96e180f89d56_s.jpg?source=06d4cd63", 121 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 122 | "https://pic2.zhimg.com/v2-c663d59d2fc308bfb967347182141144_s.jpg?source=06d4cd63", 123 | "https://pica.zhimg.com/50/v2-3b691a5ea94f7852414cd99445b50825_s.jpg?source=57bbeac9", 124 | "https://pica.zhimg.com/50/v2-1ace8b41253a8e96a503e66a3711044c_s.jpg?source=57bbeac9", 125 | "https://pica.zhimg.com/v2-d50a8f1e5040228211a978ab746cb97c_400x224.jpg?source=7e7ef6e2", 126 | "https://pic1.zhimg.com/50/v2-54153223308c3b5bbbc0b06a684a24fc_s.jpg?source=57bbeac9", 127 | "https://pic4.zhimg.com/v2-e5d7e5c7708b30ffb0dda5a15e875b82_400x224.jpg?source=7e7ef6e2", 128 | "https://pica.zhimg.com/50/v2-e8ac0ea669913f9277c13b29709507e6_s.jpg?source=57bbeac9", 129 | "https://pic1.zhimg.com/50/v2-8e0af955ca77390c9a38ad93adc19d48_s.jpg?source=57bbeac9", 130 | "https://pic1.zhimg.com/50/v2-2a79d5c9aa6648b7dd6a7212de3aa244_s.jpg?source=57bbeac9", 131 | "https://pic2.zhimg.com/50/v2-aee309294b4161a0d3f3847072e989c9_s.jpg?source=57bbeac9", 132 | "https://pic1.zhimg.com/v2-8dca6de5ed216d5a0469aef6e7b07d5c_xs.jpg?source=1940ef5c", 133 | "https://pic1.zhimg.com/v2-639e2f760fbe6dce86887936366039b2_xs.jpg?source=1940ef5c", 134 | "https://pic1.zhimg.com/v2-4ecdfffff9974056a1f2508f6f8d4750_s.jpg?source=06d4cd63", 135 | "https://pic1.zhimg.com/v2-ffd7e9f4e12f365cf141353b99d641ca_s.jpg?source=06d4cd63", 136 | "https://pic3.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 137 | "https://pic3.zhimg.com/v2-990a6baede9476b39c1a61e94ae8f505_s.jpg?source=06d4cd63", 138 | "https://pic2.zhimg.com/v2-7e20995530c81d8bb2b3f4891832ec72_s.jpg?source=06d4cd63", 139 | "https://pic1.zhimg.com/v2-f1f70ece0099a7e53caa60427f7dc446_s.jpg?source=06d4cd63", 140 | "https://pic2.zhimg.com/v2-77e09370f0998a108473fa87411950e7_s.jpg?source=06d4cd63", 141 | "https://pic2.zhimg.com/v2-8153bce8dc7079f70178c90aae4470e2_s.jpg?source=06d4cd63", 142 | "https://pic1.zhimg.com/v2-6526f5e37b1b6575d953c765c7a7af7e_s.jpg?source=06d4cd63", 143 | "https://pic3.zhimg.com/v2-639e2f760fbe6dce86887936366039b2_s.jpg?source=06d4cd63", 144 | "https://pic2.zhimg.com/v2-8464cf4efd58c6eecb6c492cf2b3600f_s.jpg?source=06d4cd63", 145 | "https://pic1.zhimg.com/v2-9c9cad920e72fe5349026dc9342efb61_s.jpg?source=06d4cd63", 146 | "https://pic2.zhimg.com/v2-36e3f4e3121b7ad4f216e5a0215c0a16_s.jpg?source=06d4cd63", 147 | "https://pic3.zhimg.com/v2-f21cf61fea0d332e2fb4ff927f5d8f77_s.jpg?source=06d4cd63", 148 | "https://pic1.zhimg.com/e34e959f10be2cb72da77b29e89d5cf1_s.jpg?source=06d4cd63", 149 | "https://pic1.zhimg.com/v2-4509864b9df97a8791e6a557e504b00f_s.jpg?source=06d4cd63", 150 | "https://pic2.zhimg.com/v2-d085816fc6b62e4fc9a3c568a2471dda_s.jpg?source=06d4cd63", 151 | "https://pic3.zhimg.com/v2-7fd59e4c33577e9700e4327328875b5c_s.jpg?source=06d4cd63", 152 | "https://pica.zhimg.com/v2-fca9a45cd0b6d0112bbc00d3d56a7f15_s.jpg?source=06d4cd63", 153 | "https://pic2.zhimg.com/v2-de5fceaaa7c7a46cd2c90589de245779_s.jpg?source=06d4cd63", 154 | "https://pic2.zhimg.com/v2-fca9a45cd0b6d0112bbc00d3d56a7f15_s.jpg?source=06d4cd63", 155 | "https://pic3.zhimg.com/v2-b60efe8f0759f0549e8a0554aa12c932_s.jpg?source=06d4cd63", 156 | "https://pic3.zhimg.com/0422735d7d5f903696fd251a621cc211_s.jpg?source=06d4cd63", 157 | "https://pica.zhimg.com/v2-b32428313c91e112834c59d934e6ee0c_s.jpg?source=06d4cd63", 158 | "https://pic1.zhimg.com/91fbc554d43f21d13e45b9f6f4bdb4f3_s.jpg?source=06d4cd63", 159 | "https://pic3.zhimg.com/v2-639e2f760fbe6dce86887936366039b2_s.jpg?source=06d4cd63", 160 | "https://pic2.zhimg.com/v2-46700d6ac95d02ff504149eaecbfefe1_s.jpg?source=06d4cd63", 161 | "https://pic1.zhimg.com/v2-b3917c99550204d56965f168b4f07a9b_s.jpg?source=06d4cd63", 162 | "https://pic1.zhimg.com/v2-1f2a667245bc6875bbe966ca4531fabd_s.jpg?source=06d4cd63", 163 | "https://pic3.zhimg.com/v2-c46137bcccd165cb7f1265e6eb430346_s.jpg?source=06d4cd63", 164 | "https://pic1.zhimg.com/v2-edc4b647ba30871d3176a01168712955_s.jpg?source=06d4cd63", 165 | "https://pic1.zhimg.com/v2-6154c49cfe4956dbba19a74904fb73b5_s.jpg?source=06d4cd63", 166 | "https://pic3.zhimg.com/v2-bbb4054bed59f25f82619a857b412d5b_s.jpg?source=06d4cd63", 167 | "https://pic3.zhimg.com/v2-639e2f760fbe6dce86887936366039b2_s.jpg?source=06d4cd63", 168 | "https://pic3.zhimg.com/v2-639e2f760fbe6dce86887936366039b2_s.jpg?source=06d4cd63", 169 | "https://pic3.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 170 | "https://pic3.zhimg.com/v2-b93ea8cf90877a3f1d7580580d5513e6_xs.jpg?source=1940ef5c", 171 | "https://pica.zhimg.com/50/v2-e380f3703a156cde201abcb7024c7aeb_720w.jpg?source=1940ef5c", 172 | "https://pic2.zhimg.com/50/v2-6c44d6cc885d011670ab7e5359635447_720w.jpg?source=1940ef5c", 173 | "https://pic4.zhimg.com/v2-1c230aa850c0638c34f868e53722b0a4_xl.jpg?source=d6434cab", 174 | "https://pic3.zhimg.com/v2-21202b1a21e66ace136fc49ef4ba0f60_xs.jpg?source=1940ef5c", 175 | "https://pic1.zhimg.com/v2-760d9fd7113acaa3495f8b7527cd4eed_xs.jpg?source=1940ef5c", 176 | "https://pic1.zhimg.com/50/v2-6ee6fffdd187d752e73056aac9a410e5_720w.jpg?source=1940ef5c", 177 | "https://pic3.zhimg.com/50/v2-6434b8660ef8ed457bab5db3598affaa_720w.jpg?source=1940ef5c", 178 | "https://pic1.zhimg.com/50/v2-7ad64de7a6e3417f132f8cd4010f2fef_720w.jpg?source=1940ef5c", 179 | "https://pic1.zhimg.com/50/v2-e18effb9c5566536b197953f4eec7917_720w.jpg?source=1940ef5c", 180 | "https://pic3.zhimg.com/50/v2-15d8651cac19885affeaddcf1f12022e_720w.jpg?source=1940ef5c", 181 | "https://pic1.zhimg.com/50/v2-1c4869196b33f38f907d9207c7a3b464_720w.jpg?source=1940ef5c", 182 | "https://pic1.zhimg.com/50/v2-17dae880555ae69851f66d50611c9329_720w.jpg?source=1940ef5c", 183 | "https://pic1.zhimg.com/50/v2-596776bb0ddc9ef7d383f0e88d5e7cc3_720w.jpg?source=1940ef5c", 184 | "https://pic1.zhimg.com/v2-2ec4709c6e8d0b4531b2c9ecef750b61_xs.jpg?source=1940ef5c", 185 | "https://pica.zhimg.com/50/v2-20e96763f8656f921d59f451197309f3_720w.jpg?source=1940ef5c", 186 | "https://pic2.zhimg.com/50/v2-6efed98ad8be294dd9ca22931a9b9584_720w.jpg?source=1940ef5c", 187 | "https://pica.zhimg.com/50/v2-07a39e031cb3f36cb8e9607abf0d2339_720w.jpg?source=1940ef5c", 188 | "https://pic1.zhimg.com/50/v2-3528e710884ce8b4ca04ab5f014554c0_720w.jpg?source=1940ef5c", 189 | "https://pic3.zhimg.com/50/v2-b7b5ddedd2f5d760edb5be589244c311_720w.jpg?source=1940ef5c", 190 | "https://pica.zhimg.com/50/v2-70fa67b65ba9569be1302499720befc6_720w.jpg?source=1940ef5c", 191 | "https://pic2.zhimg.com/50/v2-e2b14c08a9aaa1c68af7a9ddc3ef9c5a_720w.jpg?source=1940ef5c", 192 | "https://pic2.zhimg.com/50/v2-3b95d70217fc58e8cb656d0d4cc26173_720w.jpg?source=1940ef5c", 193 | "https://pic3.zhimg.com/50/v2-7cb271be4208c97086ba8dc58a16755b_720w.jpg?source=1940ef5c", 194 | "https://pic3.zhimg.com/50/v2-2697a5aef28a1eda3714fa2d46e7e8ca_720w.jpg?source=1940ef5c", 195 | "https://pica.zhimg.com/50/v2-b1ae7b7a54bb8662a9c647761eb109ad_720w.jpg?source=1940ef5c", 196 | "https://pic3.zhimg.com/50/v2-7fc5a7cd91b486cb22efbb2b118374b9_720w.jpg?source=1940ef5c", 197 | "https://pic2.zhimg.com/v2-b0da8151656f919d65b45360fadfd036_xs.jpg?source=1940ef5c", 198 | "https://pic3.zhimg.com/v2-e4ed286f648ccb4fc62abec16b3d4cd1_xs.jpg?source=1940ef5c", 199 | "https://pic2.zhimg.com/50/v2-92a246fd918f3f859051c9278c370616_720w.jpg?source=1940ef5c", 200 | "https://pic1.zhimg.com/50/v2-a159ed6961c7bfb12767cad395658c93_720w.jpg?source=1940ef5c", 201 | "https://pic1.zhimg.com/v2-d41c2ceaed8f51999522f903672a521f_xs.jpg?source=1940ef5c", 202 | "https://pic2.zhimg.com/v2-cdf8feb0151f4de58b4412bf20884135_xs.jpg?source=1940ef5c", 203 | "https://pic1.zhimg.com/50/v2-a7411b1b0065f5c6629e6983fba58057_720w.jpg?source=1940ef5c", 204 | "https://pica.zhimg.com/50/v2-fd6b23cea155a6f2f6e0cb83ab90564d_720w.jpg?source=1940ef5c", 205 | "https://pic2.zhimg.com/50/v2-317707a254b8e3167b1e1b3e18b66dce_720w.jpg?source=1940ef5c", 206 | "https://pic1.zhimg.com/50/v2-c56588957afaa808a515b52f571fcd63_720w.jpg?source=1940ef5c", 207 | "https://pic3.zhimg.com/50/v2-bc546208e682e2aba2a31154466e2141_720w.jpg?source=1940ef5c", 208 | "https://pic3.zhimg.com/50/v2-e6c38ee93a10edeb55c8751ea85601f2_720w.jpg?source=1940ef5c", 209 | "https://pic2.zhimg.com/50/v2-aed61feae9fa1db6c54782cac4644afe_720w.jpg?source=1940ef5c", 210 | "https://pic1.zhimg.com/50/v2-7377fb6364bbf6108a33feaf3a001f08_720w.jpg?source=1940ef5c", 211 | "https://pic3.zhimg.com/50/v2-be688e8c0fae323f854aacfb6aa25c12_720w.jpg?source=1940ef5c", 212 | "https://pic1.zhimg.com/50/v2-8f77ce27133a4703d0a2e2a41e3505f4_720w.jpg?source=1940ef5c", 213 | "https://pic1.zhimg.com/50/v2-493f86947161fe2af444185ee9ff67b3_720w.jpg?source=1940ef5c", 214 | "https://pic3.zhimg.com/50/v2-9690cf93f86eb25728c4d1ee8124429e_720w.jpg?source=1940ef5c", 215 | "https://pic1.zhimg.com/50/v2-6d07e6b6dfe671e12c845f4af9c0f175_720w.jpg?source=1940ef5c", 216 | "https://pic3.zhimg.com/50/v2-d29f2bbff272e85f3aac178ee34cb106_720w.jpg?source=1940ef5c", 217 | "https://pic3.zhimg.com/50/v2-519281544ac0e27b83124885aa6955fb_720w.jpg?source=1940ef5c", 218 | "https://pic3.zhimg.com/50/v2-acde677f94e049a575151d485968929f_720w.jpg?source=1940ef5c", 219 | "https://pic2.zhimg.com/50/v2-3a9734724c4f59e0d92fc2e6bf2305c8_720w.jpg?source=1940ef5c", 220 | "https://pic2.zhimg.com/50/v2-e307e36656a2d2b10cad3b9cafde40c0_720w.jpg?source=1940ef5c", 221 | "https://pica.zhimg.com/50/v2-1c2f581ed468ee186ed1a089bf08ec2b_720w.jpg?source=1940ef5c", 222 | "https://pic2.zhimg.com/50/v2-f962912040d030a7c76ce922587745d1_720w.jpg?source=1940ef5c", 223 | "https://pic3.zhimg.com/50/v2-6727ba5bf22004b60e9ed69adeff0a59_720w.jpg?source=1940ef5c", 224 | "https://pic2.zhimg.com/50/v2-b180af4137d8717788aa3dc1a43c0d12_720w.jpg?source=1940ef5c", 225 | "https://pic1.zhimg.com/50/v2-ab7e83d7e716d59d2438e2ca14cc4e10_720w.jpg?source=1940ef5c", 226 | "https://pica.zhimg.com/50/v2-f9762963e41464056e51a5f4c697be62_720w.jpg?source=1940ef5c", 227 | "https://pica.zhimg.com/50/v2-ccd645c38e26fc6d9cee69f9e0554b4d_720w.jpg?source=1940ef5c", 228 | "https://pic3.zhimg.com/50/v2-43fc891bfabdf8a1439af702d786be4a_720w.jpg?source=1940ef5c", 229 | "https://pic2.zhimg.com/v2-e51001328a8a4f3d8038945dab3dab05_s.jpg?source=06d4cd63", 230 | "https://pic3.zhimg.com/9c1655f6a0a67bb8c01ce5003f631e25_s.jpg?source=06d4cd63", 231 | "https://pic1.zhimg.com/v2-4040d4851ec15917cf31b160d26f71b8_s.jpg?source=06d4cd63", 232 | "https://pic3.zhimg.com/v2-088d706330599492af1545f29d7b5ba9_s.jpg?source=06d4cd63", 233 | "https://pic2.zhimg.com/v2-eb15f308fc644e16daa19d72b9f2e115_s.jpg?source=06d4cd63", 234 | "https://pic3.zhimg.com/v2-aa5f4c3d584cb5e396ec04e1fbdf714b_s.jpg?source=06d4cd63", 235 | "https://pic2.zhimg.com/v2-725180edd767647a1103445d01eaafec_s.jpg?source=06d4cd63", 236 | "https://pic3.zhimg.com/v2-1dbd721b6e46b4274ae613915f1d2d84_s.jpg?source=06d4cd63", 237 | "https://pic2.zhimg.com/v2-d1719f7aa358846bf535cd01d7292524_s.jpg?source=06d4cd63", 238 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 239 | "https://pic1.zhimg.com/v2-0ca42752ae810fe75872f42702b9d90c_s.jpg?source=06d4cd63", 240 | "https://pic3.zhimg.com/v2-c131f1c6f684a908142c9c028d5db498_s.jpg?source=06d4cd63", 241 | "https://pic1.zhimg.com/v2-4257438c89cd47f3130f9d93e71ed228_s.jpg?source=06d4cd63", 242 | "https://pic1.zhimg.com/v2-261671d562f3e9f27f2ee1c8ab678f41_s.jpg?source=06d4cd63", 243 | "https://pic1.zhimg.com/v2-cb977dc639e40d18f2fb01f29564afdf_s.jpg?source=06d4cd63", 244 | "https://pic3.zhimg.com/v2-275dd4cc42491fd93277179f85cfa6d5_s.jpg?source=06d4cd63", 245 | "https://pica.zhimg.com/v2-a5382203981ff2a918889b4cabcd0709_s.jpg?source=06d4cd63", 246 | "https://pica.zhimg.com/v2-39da3c90f6bf825de73eae1b6fb11167_s.jpg?source=06d4cd63", 247 | "https://pic3.zhimg.com/v2-163a4039214d1d339d131b4741fdcc81_s.jpg?source=06d4cd63", 248 | "https://pic2.zhimg.com/v2-dc41fb55c7f18086f4ac92f8bae236e4_s.jpg?source=06d4cd63", 249 | "https://pic1.zhimg.com/v2-163a4039214d1d339d131b4741fdcc81_s.jpg?source=06d4cd63", 250 | "https://pica.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 251 | "https://pica.zhimg.com/v2-b7fc3b91ecacf21e5e3ff36d15998cb8_s.jpg?source=06d4cd63", 252 | "https://pic1.zhimg.com/v2-068ac4ad33d51bff9ad421471dcfd551_s.jpg?source=06d4cd63", 253 | "https://pic3.zhimg.com/v2-0236f8f78171b0e62a57958307dd82f2_s.jpg?source=06d4cd63", 254 | "https://pic2.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_xs.jpg?source=1940ef5c", 255 | "https://pic2.zhimg.com/v2-7a966547582a50ef5d902272b1e3cbf0_xs.jpg?source=1940ef5c", 256 | "https://pic4.zhimg.com/v2-1c230aa850c0638c34f868e53722b0a4_xl.jpg?source=d6434cab", 257 | "https://pic1.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_xs.jpg?source=1940ef5c", 258 | "https://pic3.zhimg.com/50/v2-89a832593c2eda3cb1ff26cf7f3e02d1_720w.jpg?source=1940ef5c", 259 | "https://pic2.zhimg.com/50/v2-c056b8b7a9d4741effd16d1cdf621c81_720w.jpg?source=1940ef5c", 260 | "https://pic1.zhimg.com/50/v2-be18bacd194523d9ecd81038c9574df7_720w.jpg?source=1940ef5c", 261 | "https://pic1.zhimg.com/50/v2-e9622575998b98e88e21d274744157a0_720w.jpg?source=1940ef5c", 262 | "https://pic2.zhimg.com/50/v2-b82299fb0eaf2d335d31e952930af088_720w.jpg?source=1940ef5c", 263 | "https://pic2.zhimg.com/50/v2-ee041b1c78e0023aaebf1ebb83d7ad1c_720w.jpg?source=1940ef5c", 264 | "https://pica.zhimg.com/50/v2-313be3b7c15a4741c6eeb74b33eb19d3_720w.jpg?source=1940ef5c", 265 | "https://pica.zhimg.com/50/v2-5ba349468cb8dbf8fa97a562732cfa48_720w.jpg?source=1940ef5c", 266 | "https://pic3.zhimg.com/v2-a47ad82c640737fd6779aacda2867bb3_xs.jpg?source=1940ef5c", 267 | "https://pic2.zhimg.com/v2-355b55cb8511990dd6abd76fc2c06324_xs.jpg?source=1940ef5c", 268 | "https://pica.zhimg.com/50/v2-ab4336f556f9ac9c07028cba24749c65_720w.jpg?source=1940ef5c", 269 | "https://pic4.zhimg.com/v2-1c230aa850c0638c34f868e53722b0a4_xl.jpg?source=d6434cab", 270 | "https://pic2.zhimg.com/v2-3041e3c258f14671a497a7c8f3a75fba_720w.png?source=d6434cab", 271 | "https://pic3.zhimg.com/90/v2-bb7a8420e7f65fb28239d73ed16d7ee3_250x0.jpg?source=31184dd1", 272 | "https://pic2.zhimg.com/90/v2-48e086cc58d8aded23a85ac65a3b5ad4_250x0.jpg?source=31184dd1", 273 | "https://pic2.zhimg.com/90/v2-5f382e30a3448a688d9c1e869fbc8628_250x0.jpg?source=31184dd1", 274 | "https://pic4.zhimg.com/v2-bc14ea3ee0693b72c1afff70c1e271ea_720w.png?source=d6434cab", 275 | "https://pic3.zhimg.com/v2-908e081eed76bb12b147892e05aac0ef_s.jpg?source=06d4cd63", 276 | "https://pic3.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 277 | "https://pic1.zhimg.com/v2-40e069762f0c07e2235b62133210e396_s.jpg?source=06d4cd63", 278 | "https://pic2.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 279 | "https://pic1.zhimg.com/v2-e9f88bc2aa1397891743c6f0a0f8298d_s.jpg?source=06d4cd63", 280 | "https://pic3.zhimg.com/v2-61fde80850f47ccc19408e8d05591008_s.jpg?source=06d4cd63", 281 | "https://pic3.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 282 | "https://pic1.zhimg.com/v2-cd7a4c4a8070f87fafca16e7191f9029_s.jpg?source=06d4cd63", 283 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 284 | "https://pic2.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 285 | "https://pic1.zhimg.com/v2-eeff2ca4e3601907686af377606bbfa7_s.jpg?source=06d4cd63", 286 | "https://pic3.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 287 | "https://pic3.zhimg.com/v2-921d999e0f1568f85acd9e5d9cfbaa24_s.jpg?source=06d4cd63", 288 | "https://pic3.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 289 | "https://pica.zhimg.com/v2-921d999e0f1568f85acd9e5d9cfbaa24_s.jpg?source=06d4cd63", 290 | "https://pic2.zhimg.com/v2-6d864c2a408bb6f83c40397dae440f3d_s.jpg?source=06d4cd63", 291 | "https://pic1.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 292 | "https://pic3.zhimg.com/v2-b08687582cb1fb55f237c555d20f34ff_s.jpg?source=06d4cd63", 293 | "https://pic1.zhimg.com/v2-2c725e07343c133b192fad225b66897f_s.jpg?source=06d4cd63", 294 | "https://pica.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 295 | "https://pic1.zhimg.com/v2-7b29ca37c45d34705c0eb6a845a6135f_s.jpg?source=06d4cd63", 296 | "https://pic1.zhimg.com/v2-bfe6ff912de5050b6a9b9037efeb2818_s.jpg?source=06d4cd63", 297 | "https://pica.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 298 | "https://pic3.zhimg.com/v2-a18b0a713bd7017b8ec0e26a97904f37_s.jpg?source=06d4cd63", 299 | "https://pic3.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 300 | "https://pic1.zhimg.com/v2-bb99905751a30017c5b99552e950b69c_s.jpg?source=06d4cd63", 301 | "https://pic3.zhimg.com/v2-d4ae7fcb08fb6db71201065547bc397b_s.jpg?source=06d4cd63", 302 | "https://pic2.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 303 | "https://pic3.zhimg.com/v2-90de4d25de5b7938e344543b9b30e2c7_s.jpg?source=06d4cd63", 304 | "https://pic3.zhimg.com/v2-e53caef084fe36a2fedb43000b3f1e84_s.jpg?source=06d4cd63", 305 | "https://pica.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 306 | "https://pic2.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 307 | "https://pic2.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 308 | "https://pic3.zhimg.com/v2-18c0a3891b125bf69c4bf85ada323ab8_s.jpg?source=06d4cd63", 309 | "https://pica.zhimg.com/v2-848b7807b34f5b3e67c618603da9f1ff_s.jpg?source=06d4cd63", 310 | "https://pic2.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 311 | "https://pic1.zhimg.com/v2-0e7f4a6395e3ccc7db7d3d7755b59b4a_s.jpg?source=06d4cd63", 312 | "https://pic2.zhimg.com/v2-8eee503fe097d275eff004598a135bc7_s.jpg?source=06d4cd63", 313 | "https://pic2.zhimg.com/v2-f26714a0af8d577df4ea4bb08fcfef59_s.jpg?source=06d4cd63", 314 | "https://pica.zhimg.com/0fbff9f86fe8204f706aad94e7b3d6d3_s.jpg?source=06d4cd63", 315 | "https://pic1.zhimg.com/v2-abed1a8c04700ba7d72b45195223e0ff_s.jpg?source=06d4cd63", 316 | "https://pic3.zhimg.com/v2-116b6fea54a8c6a77249080747e54880_s.jpg?source=06d4cd63", 317 | ] 318 | -------------------------------------------------------------------------------- /application/utils/country_utils.py: -------------------------------------------------------------------------------- 1 | # 国家区号映射表 2 | COUNTRY_CODES = {'afghanistan': '+93', 3 | 'albania': '+355', 4 | 'algeria': '+213', 5 | 'angola': '+244', 6 | 'anguilla': '+1', 7 | 'antiguaandbarbuda': '+1268', 8 | 'argentina': '+54', 9 | 'armenia': '+374', 10 | 'aruba': '+297', 11 | 'australia': '+61', 12 | 'austria': '+43', 13 | 'azerbaijan': '+994', 14 | 'bahamas': '+1', 15 | 'bahrain': '+973', 16 | 'bangladesh': '+880', 17 | 'barbados': '+1246', 18 | 'belarus': '+375', 19 | 'belgium': '+32', 20 | 'belize': '+501', 21 | 'benin': '+229', 22 | 'bhutane': '+975', 23 | 'bih': '+387', 24 | 'bolivia': '+591', 25 | 'botswana': '+267', 26 | 'brazil': '+55', 27 | 'bulgaria': '+359', 28 | 'burkinafaso': '+226', 29 | 'burundi': '+257', 30 | 'cambodia': '+855', 31 | 'cameroon': '+237', 32 | 'canada': '+1', 33 | 'capeverde': '+238', 34 | 'caymanislands': '+1345', 35 | 'chad': '+235', 36 | 'chile': '+56', 37 | 'china': '+86', 38 | 'colombia': '+57', 39 | 'comoros': '+269', 40 | 'congo': '+243', 41 | 'costarica': '+506', 42 | 'croatia': '+385', 43 | 'cyprus': '+357', 44 | 'czech': '+420', 45 | 'djibouti': '+253', 46 | 'dominica': '+1', 47 | 'dominicana': '+1', 48 | 'easttimor': '+670', 49 | 'ecuador': '+593', 50 | 'egypt': '+20', 51 | 'england': '+44', 52 | 'equatorialguinea': '+240', 53 | 'eritrea': '+291', 54 | 'estonia': '+372', 55 | 'ethiopia': '+251', 56 | 'finland': '+358', 57 | 'france': '+33', 58 | 'frenchguiana': '+594', 59 | 'gabon': '+241', 60 | 'gambia': '+220', 61 | 'georgia': '+995', 62 | 'germany': '+49', 63 | 'ghana': '+233', 64 | 'greece': '+30', 65 | 'grenada': '+1', 66 | 'guadeloupe': '+590', 67 | 'guatemala': '+502', 68 | 'guinea': '+224', 69 | 'guineabissau': '+245', 70 | 'guyana': '+592', 71 | 'haiti': '+509', 72 | 'honduras': '+504', 73 | 'hongkong': '+852', 74 | 'hungary': '+36', 75 | 'india': '+91', 76 | 'indonesia': '+62', 77 | 'ireland': '+353', 78 | 'israel': '+972', 79 | 'italy': '+39', 80 | 'ivorycoast': '+225', 81 | 'jamaica': '+1', 82 | 'japan': '+81', 83 | 'jordan': '+962', 84 | 'kazakhstan': '+997', 85 | 'kenya': '+254', 86 | 'kuwait': '+965', 87 | 'kyrgyzstan': '+996', 88 | 'laos': '+856', 89 | 'latvia': '+371', 90 | 'lesotho': '+266', 91 | 'liberia': '+231', 92 | 'lithuania': '+370', 93 | 'luxembourg': '+352', 94 | 'macau': '+853', 95 | 'madagascar': '+261', 96 | 'malawi': '+265', 97 | 'malaysia': '+60', 98 | 'maldives': '+960', 99 | 'mauritania': '+222', 100 | 'mauritius': '+230', 101 | 'mexico': '+52', 102 | 'moldova': '+373', 103 | 'mongolia': '+976', 104 | 'montenegro': '+382', 105 | 'montserrat': '+1', 106 | 'morocco': '+212', 107 | 'mozambique': '+258', 108 | 'myanmar': '+95', 109 | 'namibia': '+264', 110 | 'nepal': '+977', 111 | 'netherlands': '+31', 112 | 'newcaledonia': '+687', 113 | 'newzealand': '+64', 114 | 'nicaragua': '+505', 115 | 'niger': '+227', 116 | 'nigeria': '+234', 117 | 'northmacedonia': '+389', 118 | 'norway': '+47', 119 | 'oman': '+968', 120 | 'pakistan': '+92', 121 | 'panama': '+507', 122 | 'papuanewguinea': '+675', 123 | 'paraguay': '+595', 124 | 'peru': '+51', 125 | 'philippines': '+63', 126 | 'poland': '+48', 127 | 'portugal': '+351', 128 | 'puertorico': '+1', 129 | 'reunion': '+262', 130 | 'romania': '+40', 131 | 'russia': '+7', 132 | 'rwanda': '+250', 133 | 'saintkittsandnevis': '+1869', 134 | 'saintlucia': '+1758', 135 | 'saintvincentandgrenadines': '+1784', 136 | 'salvador': '+503', 137 | 'samoa': '+685', 138 | 'saotomeandprincipe': '+239', 139 | 'saudiarabia': '+966', 140 | 'senegal': '+221', 141 | 'serbia': '+381', 142 | 'seychelles': '+248', 143 | 'sierraleone': '+232', 144 | 'singapore': '+65', 145 | 'slovakia': '+421', 146 | 'slovenia': '+386', 147 | 'solomonislands': '+677', 148 | 'southafrica': '+27', 149 | 'spain': '+34', 150 | 'srilanka': '+94', 151 | 'suriname': '+597', 152 | 'swaziland': '+268', 153 | 'sweden': '+46', 154 | 'switzerland': '+41', 155 | 'taiwan': '+886', 156 | 'tajikistan': '+992', 157 | 'tanzania': '+255', 158 | 'thailand': '+66', 159 | 'tit': '+1', 160 | 'togo': '+228', 161 | 'tonga': '+676', 162 | 'tunisia': '+216', 163 | 'turkey': '+90', 164 | 'turkmenistan': '+993', 165 | 'turksandcaicos': '+1', 166 | 'uganda': '+256', 167 | 'ukraine': '+380', 168 | 'uruguay': '+598', 169 | 'usa': '+1', 170 | 'uzbekistan': '+998', 171 | 'venezuela': '+58', 172 | 'vietnam': '+84', 173 | 'virginislands': '+1', 174 | 'zambia': '+260', 175 | 'zimbabwe': '+263'} 176 | 177 | # 取号列表集合 178 | AREA_CODES = list(COUNTRY_CODES.values()) 179 | 180 | # 区号-国家名映射表 181 | CODE_COUNTRY_DICT = {v: k for k, v in COUNTRY_CODES.items()} 182 | 183 | 184 | def get_country_phone_code(country: str) -> str: 185 | return COUNTRY_CODES.get(country) 186 | 187 | 188 | def split_code_phone(phone: str) -> tuple: 189 | """ 190 | 将带有区号的手机号切分成 区号, 手机号 191 | 例:+79771760219 192 | return: (7, 9771760219) 193 | """ 194 | for code in AREA_CODES: 195 | if phone.startswith(code): 196 | return code[1:], phone.replace(code, "") 197 | -------------------------------------------------------------------------------- /application/utils/json_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask.json import JSONEncoder 4 | import enum 5 | import decimal 6 | from application.models.base import BaseModel 7 | 8 | 9 | class CustomJSONEncoder(JSONEncoder): 10 | 11 | def default(self, obj): 12 | try: 13 | if isinstance(obj, BaseModel): 14 | return obj.to_dict() 15 | elif isinstance(obj, enum.Enum): 16 | return obj.value 17 | elif type(obj) is datetime.date: 18 | return datetime.datetime(obj.year, obj.month, obj.day).isoformat() 19 | elif type(obj) is datetime.datetime: 20 | return obj.isoformat() 21 | elif type(obj) is datetime.time: 22 | return str(obj) 23 | elif isinstance(obj, decimal.Decimal): 24 | return float(obj) 25 | # 时间区间 26 | elif isinstance(obj, datetime.timedelta): 27 | return int(obj.total_seconds() * 1000) 28 | else: 29 | return str(obj) 30 | # iterable = iter(obj) 31 | except TypeError: 32 | pass 33 | # else: 34 | # return list(iterable) 35 | return JSONEncoder.default(self, obj) 36 | -------------------------------------------------------------------------------- /application/utils/jwt_utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import current_app, request 4 | from flask_jwt_extended import ( 5 | verify_jwt_in_request, create_access_token, get_current_user, 6 | get_jwt_identity, create_refresh_token 7 | ) 8 | 9 | from application.errors import AuthorizationError 10 | from application.errors import LoginInAnotherDeviceError, AuthDeleteError, AuthForbiddenError 11 | from application.extensions import authapi 12 | from application.models.user import InstitutionUser, StudentUser, Staff, User 13 | 14 | 15 | def create_jwt_token(user, expires_delta=None, platform=None): 16 | """创建jwt token""" 17 | return _create_token("access_token", user, expires_delta=expires_delta, platform=platform) 18 | 19 | 20 | def create_jwt_refresh_token(user, expires_delta=None, platform=None): 21 | """创建jwt refresh token""" 22 | return _create_token("refresh_token", user, expires_delta=expires_delta, platform=platform) 23 | 24 | 25 | def _create_token(token_type, user, expires_delta=None, platform=None): 26 | if isinstance(user, InstitutionUser): 27 | identity = {'type': 'institution', 'id': user.id, 'platform': platform} 28 | elif isinstance(user, StudentUser): 29 | identity = {'type': 'student', 'id': user.id, 'platform': platform} 30 | elif isinstance(user, Staff): 31 | identity = {'type': 'staff', 'id': user.id, 'platform': platform} 32 | elif isinstance(user, User): 33 | # 此时的user是没有关联机构的的,上面的都是关联了具体的机构的token 34 | identity = {'type': 'user', 'id': user.id, 'platform': platform} 35 | else: 36 | raise ValueError("Invalid user type") 37 | if token_type == 'access_token': 38 | return create_access_token(identity, expires_delta=expires_delta) 39 | else: 40 | return create_refresh_token(identity, expires_delta=expires_delta) 41 | 42 | 43 | def login_required(user_type): 44 | """检查是否登录""" 45 | 46 | def login_wrapper(fn): 47 | @wraps(fn) 48 | def wrapper(*args, **kwargs): 49 | verify_jwt_in_request() 50 | identity = get_jwt_identity() or {} 51 | 52 | if user_type and identity.get('type') != user_type: 53 | raise AuthorizationError(msg="非法用户类型") 54 | else: 55 | return fn(*args, **kwargs) 56 | 57 | return wrapper 58 | 59 | return login_wrapper 60 | 61 | 62 | # 校验jwt是否是合法的机构用户 63 | # institution_user_required = login_required('institution') 64 | 65 | institution_user_required2 = authapi.user_required(role='institution') 66 | institution_user_required = institution_user_required2 67 | # 校验jwt是否是合法的学员 68 | student_user_required = login_required('student') 69 | # 校验jwt是否是合法的员工 70 | staff_user_required = authapi.user_required(role='staff') 71 | # 账号登录 72 | user_required = login_required('user') # 还没关联机构的登录 73 | # 不限制用户类型,任意用户类型 74 | any_user_required = login_required(None) 75 | 76 | 77 | def loader_user(identity): 78 | """通过jwt identity获取用户""" 79 | user_type = identity.get('type') 80 | user_id = identity.get('id') 81 | platform = identity.get('platform') 82 | if user_type == 'institution': 83 | institution_user = InstitutionUser.query.get(user_id) 84 | if not institution_user: 85 | return institution_user 86 | if institution_user.is_deleted: 87 | raise AuthDeleteError("您的帐号暂无权限,请联系管理员~") 88 | if not institution_user.is_enabled: 89 | raise AuthForbiddenError("您的帐号暂无权限,请联系管理员~") 90 | institution_user.check_roles() 91 | return institution_user 92 | if user_type == 'user': 93 | user = User.query.get(user_id) 94 | return user 95 | if user_type == 'student': 96 | student_user = StudentUser.query.get(user_id) 97 | if not student_user: 98 | return None 99 | if student_user.is_deleted: 100 | raise AuthDeleteError("您的帐号暂无权限,请联系管理员~") 101 | if not student_user.is_enabled: 102 | raise AuthForbiddenError("您的帐号暂无权限,请联系管理员~") 103 | auth_header = request.headers.get(current_app.config.get('JWT_HEADER_NAME'), "").replace("Bearer ", "") 104 | if "/refresh" not in request.path: 105 | # refresh 接口使用的refresh_token,不做判断 106 | # h5 平台 和 pc平台要求各自登录互踢 107 | if platform == 'h5' and auth_header != student_user.h5_token: 108 | raise LoginInAnotherDeviceError( 109 | error_info=f"platform:{platform} \nrequest_token:{auth_header}\n token:{student_user.h5_token}") 110 | if platform == 'pc' and auth_header != student_user.pc_token: 111 | raise LoginInAnotherDeviceError( 112 | error_info=f"platform:{platform} \nrequest_token:{auth_header}\n token:{student_user.pc_token}") 113 | return student_user 114 | elif user_type == 'staff': 115 | return Staff.query.get(user_id) 116 | else: 117 | return None 118 | 119 | 120 | def get_institution_user() -> InstitutionUser: 121 | """获取当前登录的机构用户""" 122 | # return get_current_user() 123 | return get_institution_user2() 124 | 125 | def get_institution_user2() -> InstitutionUser: 126 | """获取当前登录的机构用户""" 127 | from application.contrib.authapi import get_current_user 128 | return get_current_user() 129 | 130 | 131 | def get_staff_user() -> Staff: 132 | """获取当前登录的内部员工""" 133 | # return get_current_user() 134 | from application.contrib.authapi import get_current_user 135 | return get_current_user() 136 | 137 | 138 | def get_student_user() -> StudentUser: 139 | """获取当前登录的内部员工""" 140 | return get_current_user() 141 | -------------------------------------------------------------------------------- /application/utils/list_utils.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | from typing import List 3 | 4 | 5 | def key_by(items, key): 6 | key_dict = {} 7 | for item in items: 8 | key_dict[getattr(item, key)] = item 9 | return key_dict 10 | 11 | 12 | def group_by(items, key): 13 | group_dict = {} 14 | for item in items: 15 | value = getattr(item, key) 16 | if not group_dict.get(value): 17 | group_dict[value] = [] 18 | group_dict[value].append(item) 19 | return group_dict 20 | 21 | 22 | def list_to_tree(source: List[dict]): 23 | """ 24 | 将元素中包含有父节点的列表构造转化成一颗带有子节点对象的树状列表结构 25 | 26 | source = [ 27 | {"id": 1, "name": "A", "parent_id": None}, 28 | {"id": 2, "name": "B", "parent_id": 1}, 29 | {"id": 3, "name": "C", "parent_id": 2}, 30 | {"id": 4, "name": "D", "parent_id": 3}, 31 | {"id": 5, "name": "E", "parent_id": 2}, 32 | ] 33 | 转化为 34 | 35 | target=[{"id": 1, 36 | "name": "A", 37 | "children": [{"id": 2, 38 | "name": "B", 39 | "children": [{"id": 3, 40 | "name": "C", 41 | "children": [{"id": 4, 42 | "name": "D", 43 | "children": []}]}, 44 | {"id": 5, 45 | "name": "E", 46 | "children": []}]}]}] 47 | 48 | """ 49 | # 先把列表变成字典(key 是id,value 是列表元素 50 | source_dict = {item.get("id"): item for item in source} 51 | target = [] 52 | for item in source: 53 | if not item.get("parent_id"): 54 | 55 | # 第0级别直接加到target 56 | target.append(item) 57 | else: 58 | # 如果当前元素是子节点,则找到父元素后,加入到父元素的children属性里面 59 | parent = source_dict.get(item.get("parent_id")) 60 | if parent: 61 | parent.setdefault("children", []).append(item) 62 | else: 63 | # 即使有parent_id,也有可能没有parent_id的权限,当前节点就当作父节点 64 | target.append(item) 65 | return target 66 | 67 | 68 | def flatten(itr: List[list]): 69 | """ 70 | 打平列表 71 | [[1, 2, 3], [4, 5, 6], [7], [8, 9]] 72 | to 73 | [1, 2, 3, 4, 5, 6, 7, 8, 9] 74 | """ 75 | from functools import reduce 76 | import operator 77 | if itr: 78 | return reduce(operator.concat, itr) 79 | else: 80 | return itr 81 | 82 | 83 | def all_children_codes(node: dict, authrys: List[str], check=True): 84 | """ 85 | 获取 node下指定节点authrys的所有的子节点 86 | 87 | node = { 88 | "code": '01', 89 | "children": [ 90 | { 91 | "code": '11', 92 | "children": [{ 93 | "code": '111', 94 | }]}, 95 | { 96 | "code": '22', 97 | "children": [{ 98 | "code": '222', 99 | }] 100 | }, 101 | ], 102 | } 103 | 返回: ["01", "11", "21", '111', '222'] 104 | :param node 当前节点 105 | :param authrys 需要获取的节点的code值 106 | :param check 107 | :return list 108 | """ 109 | result: List[str] = [] 110 | need_check = True 111 | if not check or (node.get("code") in authrys): 112 | result.append(node.get("code")) 113 | need_check = False 114 | if node.get("children"): 115 | children = map(lambda item: all_children_codes(item, authrys, need_check), node.get("children")) 116 | 117 | result.extend(flatten(children)) 118 | 119 | return result 120 | -------------------------------------------------------------------------------- /application/utils/md5_utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def md5_hash(content): 5 | md5 = hashlib.md5() 6 | md5.update(content.encode("utf-8")) 7 | password = md5.hexdigest() 8 | return str(password) 9 | 10 | 11 | def generate_password_hash(plaintext, salt=None): 12 | md5 = hashlib.md5() 13 | md5.update(plaintext.encode("utf-8")) 14 | password = md5.hexdigest() 15 | if salt: 16 | plaintext = password + salt 17 | md5 = hashlib.md5() 18 | md5.update(plaintext.encode("utf-8")) 19 | password = md5.hexdigest() 20 | 21 | return str(password) 22 | 23 | 24 | def check_password_hash(pass_hash, password, salt=None): 25 | md5 = hashlib.md5() 26 | md5.update(password.encode("utf-8")) 27 | hash_str = md5.hexdigest() 28 | if salt: 29 | plaintext = hash_str + salt 30 | md5 = hashlib.md5() 31 | md5.update(plaintext.encode("utf-8")) 32 | hash_str = md5.hexdigest() 33 | return pass_hash == hash_str 34 | -------------------------------------------------------------------------------- /application/utils/parse_utils.py: -------------------------------------------------------------------------------- 1 | from dateutil.parser import parse 2 | 3 | 4 | def parse_datetime(string): 5 | return parse(string) 6 | 7 | 8 | def parse_date(string): 9 | return parse(string).date() 10 | 11 | 12 | def parse_bool(string): 13 | if string == '0': 14 | return False 15 | else: 16 | return True 17 | -------------------------------------------------------------------------------- /application/utils/random_utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import time 4 | import uuid 5 | 6 | 7 | 8 | def random_order_number(prefix=None): 9 | """生成随机订单号""" 10 | 11 | def f(): 12 | r = int(time.time() * 1000) 13 | if prefix: 14 | r = str(prefix) + str(r) 15 | r = str(r) 16 | return r 17 | 18 | return f 19 | 20 | 21 | def random_str(length=16, prefix=""): 22 | """ 23 | 生成随机字符串, 由大小写字母和数字组成 24 | :param length: 25 | :param prefix: 前缀 26 | """ 27 | return prefix + ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length)) 28 | 29 | 30 | def random_num_str(length=4): 31 | """ 32 | 随机数字 33 | """ 34 | return "".join([str(random.randint(0, 9)) for i in range(length)]) 35 | 36 | 37 | def random_base16_num_str(length=16): 38 | return ''.join(random.choice("0123456789abcdef") for _ in range(length)) 39 | 40 | 41 | def random_uuid_str(): 42 | return uuid.uuid1().hex[:16] 43 | 44 | 45 | def random_mac_address(): 46 | mac = [random.randrange(256) for _ in range(6)] 47 | mac = ":".join('%02x' % b for b in mac) 48 | return mac 49 | 50 | 51 | def random_en_name(): 52 | return faker.Faker().name() 53 | 54 | 55 | def objectid(): 56 | return str(ObjectId()) 57 | -------------------------------------------------------------------------------- /application/utils/request_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Dict 2 | import urllib.parse as urlparse 3 | from pydantic import BaseModel 4 | from werkzeug.datastructures import MultiDict 5 | 6 | 7 | def convert_query_params(query_prams: MultiDict, model: Type[BaseModel]) -> dict: 8 | """ 9 | :param query_prams: flask request.args 10 | :param model: query parameter's model 11 | :return resulting parameters 12 | """ 13 | return { 14 | **query_prams.to_dict(), 15 | **{key: value for key, value in query_prams.to_dict(flat=False).items() if 16 | key in model.__fields__ and model.__fields__[key].is_complex()} 17 | } 18 | 19 | 20 | def get_url_queries(url, *args) -> Dict: 21 | """ 22 | 提取URL中的查询参数 23 | :param url: 24 | :param args: 参数,可传多个 25 | :return: 26 | """ 27 | parsed = urlparse.urlparse(url) 28 | queries = urlparse.parse_qs(parsed.query) 29 | return {k: v[0] for k, v in queries.items() if k in args} 30 | 31 | def get_client_ip(): 32 | from flask import request 33 | return request.environ.get('HTTP_X_FORWARDED_FOR') or request.environ.get('HTTP_X_REAL_IP') or request.environ.get( 34 | 'REMOTE_ADDR') 35 | -------------------------------------------------------------------------------- /application/utils/response_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import string 4 | 5 | 6 | from flask import current_app, send_file 7 | 8 | from application.contants import DEFAULT_ERROR_CODE 9 | from application.errors import ERROR_CODE 10 | 11 | 12 | def error(data=None, code=DEFAULT_ERROR_CODE, msg=None, http_code=400, error_info=None): 13 | """ 14 | 根据错误信息构造对应body 15 | """ 16 | result = { 17 | 'data': data, 18 | 'code': code, 19 | 'msg': msg or ERROR_CODE.get(code, ""), 20 | 'status': 'error', 21 | } 22 | if current_app.config.get("ENV") != 'production': 23 | result['error_info'] = error_info 24 | return result, http_code 25 | 26 | 27 | def success(data=None, code=200, http_code=200): 28 | """ 29 | 组装成功时候的body 30 | :param data: 返回的信息 31 | :param code: 业务状态码 32 | :param http_code: http状态码 33 | :return: json格式错误提示字符串 34 | """ 35 | result = {'code': code, 'data': data, 'msg': 'success'} 36 | return result, http_code 37 | 38 | 39 | def export(file_name: str, column_fields: list, row_data: list): 40 | """ 41 | 导出文件 42 | """ 43 | import xlsxwriter 44 | file_name = file_name.replace("/", "") 45 | file_name = f'{file_name}{datetime.datetime.now().strftime("%Y%m%d%H%M%S")}.xlsx' 46 | file_path = os.path.join(current_app.config['TEMP_DIR'], file_name) 47 | workbook = xlsxwriter.Workbook(file_path) 48 | worksheet = workbook.add_worksheet("数据") 49 | bold = workbook.add_format({'bold': True}) 50 | for index, column in enumerate(column_fields): 51 | worksheet.write(string.ascii_uppercase[index] + "1", column, bold) 52 | row = 1 53 | for item in row_data: 54 | for i, e in enumerate(item): 55 | worksheet.write(row, i, e) 56 | row += 1 57 | workbook.close() 58 | return send_file(file_path, attachment_filename=file_name, as_attachment=True) 59 | -------------------------------------------------------------------------------- /application/utils/time_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dateutil.relativedelta import relativedelta 4 | 5 | 6 | def str_to_time(value): 7 | return datetime.time.fromisoformat("%H:%M") 8 | 9 | 10 | def str_to_date(value, format="%Y-%m-%d"): 11 | if value is None: 12 | return None 13 | return datetime.datetime.strptime(value, format).date() 14 | 15 | 16 | def str_to_datetime(value, format="%Y-%m-%d %H:%M:%S"): 17 | if value is None: 18 | return None 19 | return datetime.datetime.strptime(value, format) 20 | 21 | 22 | def format_to_date_number(dt: datetime.datetime) -> int: 23 | """ 24 | 将时间格式化成int整数,如 20221212 25 | :param dt: 26 | """ 27 | return int(dt.strftime("%Y%m%d")) 28 | 29 | 30 | def timestamp_to_datetime(value, unit="ms"): 31 | unit = 1000 if unit == 'ms' else 1 32 | return datetime.datetime.fromtimestamp(value / unit) 33 | 34 | 35 | def format_time_delta(seconds): 36 | if seconds is None: 37 | return None 38 | m, s = divmod(seconds, 60) 39 | h, m = divmod(m, 60) 40 | if h: 41 | return f'{h:d}小时{m:d}分{s:d}秒' 42 | elif m: 43 | return f'{m:d}分{s:d}秒' 44 | else: 45 | return f'{s:d}秒' 46 | 47 | 48 | def first_day_of_pre_month(dt: datetime.datetime): 49 | """ 50 | 获取上一个月的第一天 51 | """ 52 | return (dt.replace(day=1) - datetime.timedelta(days=1)).replace(day=1, hour=0, minute=0, second=0) 53 | 54 | 55 | def first_day_of_month(dt: datetime.datetime) -> datetime.datetime: 56 | return dt.replace(day=1) 57 | 58 | 59 | def last_day_of_month(any_day: datetime.datetime): 60 | """ 61 | 根据任何时间获取同月的最后一天 62 | return: datetime.datetime 63 | """ 64 | return (any_day + relativedelta(day=31)).replace(hour=23, minute=59, second=59) 65 | 66 | 67 | def first_day_fo_pre_week(dt: datetime.datetime) -> datetime.datetime: 68 | """ 69 | 获取上周的第一天 70 | """ 71 | return first_day_of_week(dt) - datetime.timedelta(days=7) 72 | 73 | 74 | def first_day_of_week(dt: datetime.datetime) -> datetime.datetime: 75 | """ 76 | 获取当天所在周的第一天 77 | """ 78 | return dt - datetime.timedelta(days=dt.weekday()) 79 | 80 | 81 | def last_day_of_week(dt: datetime.datetime) -> datetime.datetime: 82 | """ 83 | 获取当天所在周的第一天 84 | """ 85 | return dt + datetime.timedelta(days=6 - dt.weekday()) 86 | 87 | def days_of_month(year, month): 88 | """ 89 | 获取指定月份的天数 90 | """ 91 | from calendar import monthrange 92 | num_days = monthrange(year, month)[1] 93 | return num_days 94 | 95 | 96 | def ceil_day(d: datetime.datetime) -> datetime.datetime: 97 | """ 98 | 获取指定时间的当天的最后时刻 99 | :param d: 100 | :return: 101 | """ 102 | return d.replace(hour=23, minute=59, second=59, microsecond=9999) 103 | 104 | 105 | def floor_day(d: datetime.datetime) -> datetime.datetime: 106 | """ 107 | 获取指定时间的当天的开始时刻 108 | :param d: 109 | :return: 110 | """ 111 | return d.replace(hour=0, minute=0, second=0, microsecond=0) 112 | -------------------------------------------------------------------------------- /gunicorn.conf: -------------------------------------------------------------------------------- 1 | # 并行工作进程数 2 | workers = 1 3 | # 指定每个工作者的线程数 4 | threads = 1 5 | # 监听内网端口80 6 | bind = '0.0.0.0:80' 7 | # 设置最大并发量 8 | worker_connections = 2000 9 | # 设置进程文件目录 10 | pidfile = 'gunicorn.pid' 11 | # 设置访问日志和错误信息日志路径 12 | accesslog = 'log/gunicorn_access.log' 13 | errorlog = 'log/gunicorn_error.log' 14 | # 设置日志记录水平 15 | loglevel = 'info' 16 | worker_class = "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" 17 | capture_output = True 18 | -------------------------------------------------------------------------------- /image-20230216152404777.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/image-20230216152404777.png -------------------------------------------------------------------------------- /qrcode_gh_4340d45cdd5f_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/qrcode_gh_4340d45cdd5f_1.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==2.1.0 2 | python-dotenv==0.20.0 3 | supervisor==4.2.4 4 | gunicorn==20.1.0 5 | pydantic==1.9.0 6 | flask_wechatpy==0.1.3 7 | flask-siwadoc==0.1.8 8 | pycryptodome==3.17 9 | openai==0.27.0 10 | flask_testing 11 | flask_redis==0.4.0 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import logging 4 | import unittest 5 | 6 | from flask import current_app 7 | 8 | from application import create_app 9 | from application.contants import DEFAULT_ERROR_CODE 10 | from application.enum_field import CourseKind 11 | from application.utils.random_utils import random_str 12 | from .base.http import HTTPBaseTestCase 13 | 14 | 15 | class BaseTestCase(HTTPBaseTestCase): 16 | 17 | def create_app(self): 18 | app = create_app("testing") 19 | logger = logging.getLogger() 20 | logger.setLevel(level=logging.INFO) 21 | return app 22 | 23 | def setUp(self): 24 | """创建所有表""" 25 | 26 | pass 27 | 28 | def tearDown(self): 29 | """清空session并删除所有表""" 30 | pass 31 | 32 | 33 | 34 | 35 | 36 | if __name__ == '__main__': 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /tests/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/tests/base/__init__.py -------------------------------------------------------------------------------- /tests/base/http.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import json 4 | import urllib.parse 5 | 6 | from flask_testing import TestCase 7 | 8 | 9 | class HTTPBaseTestCase(TestCase): 10 | 11 | def post(self, url, headers={}, auth_user=None, refresh_token=False, **kwargs): 12 | """ 13 | POST 请求API, 会加上认证信息 14 | """ 15 | if auth_user: 16 | headers['Authorization'] = "Bearer " + (create_jwt_token( 17 | auth_user) if not refresh_token else create_jwt_refresh_token(auth_user)) 18 | response = self.client.post(url, headers=headers, **kwargs) 19 | return response 20 | 21 | def put(self, url, headers={}, auth_user=None, **kwargs): 22 | """ 23 | PUT请求API, 会加上认证信息 24 | """ 25 | if auth_user: 26 | headers['Authorization'] = "Bearer " + create_jwt_token(auth_user) 27 | 28 | response = self.client.put(url, headers=headers, **kwargs) 29 | return response 30 | 31 | def get(self, url, headers=None, auth_user=None, params=None, **kwargs): 32 | """ 33 | GET请求API, 会加上认证信息 34 | """ 35 | if not headers: 36 | headers = dict() 37 | 38 | def format_value(value): 39 | if isinstance(value, datetime.datetime): 40 | return value.isoformat() 41 | if isinstance(value, datetime.datetime): 42 | return value.isoformat() 43 | if isinstance(value, bool): 44 | if value: 45 | return 1 46 | else: 47 | return 0 48 | 49 | return value 50 | 51 | if params: 52 | params = {k: format_value(v) for k, v in params.items()} 53 | s = urllib.parse.urlencode(params, doseq=True) # 支持数组解析 54 | url = url + "?" + s 55 | 56 | if auth_user: 57 | headers['Authorization'] = "Bearer " + create_jwt_token(auth_user) 58 | 59 | response = self.client.get(url, headers=headers, **kwargs) 60 | return response 61 | 62 | def delete(self, url, headers={}, auth_user=None, **kwargs): 63 | """ 64 | DELETE请求API, 会加上认证信息 65 | """ 66 | if auth_user: 67 | headers['Authorization'] = "Bearer " + create_jwt_token(auth_user) 68 | 69 | response = self.client.delete(url, headers=headers, **kwargs) 70 | db.session.expire_all() 71 | return response 72 | 73 | def assertSuccess(self, response): 74 | """ 75 | 检查是否返回成功 76 | """ 77 | 78 | self.assert200(response) 79 | self.assertEqual(response.json['code'], 200) 80 | 81 | def assertError(self, response, code): 82 | """ 83 | 检查返是否返回正确参数错误返回值 84 | """ 85 | print(response.json) 86 | self.assertEqual(response.json['code'], code) 87 | self.assertEqual(response.json['status'], 'error') 88 | 89 | def json(self, response): 90 | data = json.loads(response.data) 91 | return json.dumps(data) 92 | 93 | def pprint_response(self, response): 94 | import pprint 95 | pprint.pprint(json.loads(response.data)) 96 | 97 | def data(self, response): 98 | return json.loads(response.data).get("data") 99 | -------------------------------------------------------------------------------- /tests/extension/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzjun567/chatgpt-gzh/cbc783edb4093fdc07962fa4e956293d2d9df260/tests/extension/__init__.py -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import argparse 3 | import os 4 | import sys 5 | 6 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 7 | if BASE_DIR not in sys.path: 8 | sys.path.append(BASE_DIR) 9 | 10 | from application import create_app 11 | 12 | app = create_app() 13 | 14 | if __name__ == "__main__": 15 | parser = argparse.ArgumentParser(description='MV') 16 | parser.add_argument('--port', action="store", dest="port", type=int, default=5000) 17 | args = parser.parse_args() 18 | host = '0.0.0.0' 19 | port = args.port 20 | app.logger.info("listen on %s:%s" % (host, port)) 21 | app.run(host=host, port=port) 22 | --------------------------------------------------------------------------------