├── .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 | 
46 |
47 |
48 |
49 | ### 体验地址
50 |
51 | 关注公众号【志军foofish】直接发起提问
52 |
53 | 
54 |
55 |
56 |
57 |
58 | ### 联系我
59 |
60 | 微信:lzjun567
61 |
62 | 
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------