├── .github
├── ISSUE_TEMPLATE
│ ├── ----.md
│ ├── bug.md
│ ├── help.md
│ └── improvement.md
└── workflows
│ └── main.yml
├── .gitignore
├── .pylintrc
├── LICENSE
├── README.md
├── config.py
├── demo
└── hello
│ ├── README.md
│ ├── config.py
│ ├── http.jpg
│ ├── run.py
│ └── weixin.jpg
├── docs
├── accesspoint.py
├── accesspoints.json
├── accesspoints.md
├── api
│ ├── api.commodity.html
│ ├── api.commodity.md
│ ├── api.rbac.html
│ └── api.rbac.md
├── deployment.md
├── errcode.json
├── errcode.md
├── errcode.py
├── illustration
│ ├── JWT验证.drawio
│ ├── JWT验证.png
│ ├── 事件管理器.drawio
│ ├── 事件管理器.png
│ ├── 支付流程.drawio
│ └── 支付流程.png
├── introduction.md
├── logo-bar.png
├── other.md
└── public
│ ├── logo-bar.drawio
│ ├── logo-bar.png
│ ├── logo-bar.svg
│ ├── logo-cropped.svg
│ ├── logo.old.drawio
│ ├── logo.png
│ ├── logo.svg
│ ├── social-page.png
│ └── structure.drawio
├── leaf
├── __init__.py
├── api
│ ├── __init__.py
│ ├── error.py
│ ├── settings.py
│ ├── validator.py
│ └── wrapper.py
├── core
│ ├── __init__.py
│ ├── abstract
│ │ ├── __init__.py
│ │ ├── payment.py
│ │ └── plugin.py
│ ├── algorithm
│ │ ├── __init__.py
│ │ ├── concdict.py
│ │ ├── fsm.py
│ │ ├── keydict.py
│ │ └── tree.py
│ ├── database.py
│ ├── error.py
│ ├── events.py
│ ├── parallel.py
│ ├── schedule.py
│ ├── tools
│ │ ├── __init__.py
│ │ ├── encrypt.py
│ │ ├── file.py
│ │ ├── time.py
│ │ └── web.py
│ └── wrapper.py
├── files
│ ├── static
│ │ └── README.md
│ └── uploads
│ │ └── README.md
├── payments
│ ├── __init__.py
│ ├── alipay
│ │ └── __init__.py
│ ├── wxpay
│ │ ├── __init__.py
│ │ ├── const.py
│ │ ├── error.json
│ │ ├── error.py
│ │ ├── methods.py
│ │ ├── payment.py
│ │ ├── settings.py
│ │ └── signature.py
│ └── yandex
│ │ └── __init__.py
├── plugins
│ ├── __init__.py
│ ├── accesstoken
│ │ └── __init__.py
│ ├── error.py
│ ├── manager.py
│ └── settings.py
├── rbac
│ ├── __init__.py
│ ├── error.py
│ ├── functions
│ │ ├── __init__.py
│ │ ├── accesspoint.py
│ │ ├── auth.py
│ │ ├── group.py
│ │ └── user.py
│ ├── jwt
│ │ ├── __init__.py
│ │ ├── const.py
│ │ ├── settings.py
│ │ ├── token.py
│ │ └── verify.py
│ ├── model
│ │ ├── __init__.py
│ │ ├── accesspoint.py
│ │ ├── auth.py
│ │ ├── group.py
│ │ └── user.py
│ └── settings.py
├── selling
│ ├── __init__.py
│ ├── commodity
│ │ ├── __init__.py
│ │ ├── generator.py
│ │ ├── product.py
│ │ └── stock.py
│ ├── error.py
│ ├── functions
│ │ ├── __init__.py
│ │ ├── product.py
│ │ └── stock.py
│ ├── order
│ │ ├── __init__.py
│ │ ├── events.py
│ │ ├── manager.py
│ │ ├── order.py
│ │ ├── settings.py
│ │ └── status.py
│ └── settings.py
├── views
│ ├── __init__.py
│ ├── commodity
│ │ ├── __init__.py
│ │ ├── product.py
│ │ └── stock.py
│ ├── order.py
│ ├── plugins.py
│ ├── rbac
│ │ ├── __init__.py
│ │ ├── accesspoint.py
│ │ ├── auth.py
│ │ ├── group.py
│ │ ├── jwt.py
│ │ └── user.py
│ ├── weixin.py
│ └── wxpay.py
└── weixin
│ ├── __init__.py
│ ├── accesstoken
│ ├── __init__.py
│ ├── const.py
│ ├── error.py
│ ├── events.py
│ ├── patch.py
│ └── settings.py
│ ├── apis
│ ├── __init__.py
│ ├── template.py
│ └── user.py
│ ├── const.py
│ ├── encrypt.py
│ ├── errcodes.json
│ ├── error.py
│ ├── reply
│ ├── __init__.py
│ ├── event.py
│ ├── maker.py
│ ├── message.py
│ └── url.py
│ └── settings.py
├── linting.py
├── requirements.txt
├── run.py
└── setup.py
/.github/ISSUE_TEMPLATE/----.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 功能建议
3 | about: 为本项目提供您的意见
4 | title: "[Improvement]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **您的意见是否由一个错误导致**
11 | 如果是,您可以考虑提交 bug 类型的 issue
12 |
13 | **描述您希望的改进**
14 | 在此详细的描述您希望的改进,他们可能是:
15 | - 功能增加
16 | - 现有功能的改进
17 | - 其他
18 |
19 | **实现方案/思路**
20 | 如果您有实现方案/思路,请在此提出
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug 报告
3 | about: 告诉我们使用过程中发现的bug
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Bug 描述**
11 | 请简要的描述您遇到的问题,包含:
12 | 1. 您预期的行为
13 | 2. 实际发生的行为
14 |
15 | **是否可以复现**
16 | 是/否
17 |
18 | **如何复现**
19 | 如果可以复现,请简要描述复现过程
20 |
21 | **环境信息**
22 | - OS
23 | - Python版本
24 | - Version
25 | - 使用依赖库版本
26 |
27 | **额外信息**
28 | 如果有日志,请给出相关部分
29 | 您如果有对此bug的修复建议,可以在此提出
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 需要帮助
3 | about: 如果您需要使用帮助请在此提出
4 | title: "[Help]"
5 | labels: help wanted
6 | assignees: ''
7 |
8 | ---
9 |
10 | **问题描述**
11 | 简要的描述需要帮助解决的问题/疑问
12 |
13 | **环境信息**
14 | *可选的* 留下您所使用的环境/版本信息以便我们更好地帮助您
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/improvement.md:
--------------------------------------------------------------------------------
1 | --- ---
2 | name: 功能建议
3 | about: 为本项目提供您的意见
4 | title: "[Improvement]"
5 | labels: 'enhancement'
6 | assignees: ''
7 |
8 |
9 | --- ---
10 |
11 |
12 | **您的意见是否由一个错误导致**
13 | 如果是,您可以考虑提交 bug 类型的 issue
14 |
15 |
16 | **描述您希望的改进**
17 | 在此详细的描述您希望的改进,他们可能是:
18 | - 功能增加
19 | - 现有功能的改进
20 | - 其他
21 |
22 | **实现方案/思路**
23 | 如果您有实现方案/思路,请在此提出
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Test and Pylint
2 |
3 | on: [push]
4 |
5 | jobs:
6 | linting:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 | id: checkout
14 |
15 | - name: Setup Python
16 | uses: actions/setup-python@v1
17 | with:
18 | python-version: 3.7
19 |
20 | - name: Install dependencies
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install -r requirements.txt
24 | pip install pylint
25 |
26 | - name: Test modules
27 | run: python run.py
28 | id: test
29 |
30 | - name: Pylinting
31 | if: success()
32 | run: python linting.py
33 | id: linting
34 |
35 | - name: Low score warning
36 | if: steps.linting.outputs.score < 9
37 | run: echo "Test passed, but score was too low."
38 |
39 | - name: Passed
40 | if: steps.linting.outputs.score >= 9
41 | run: echo "All passed."
42 |
43 | - name: Show score
44 | run: echo "Your score is ${{ steps.linting.outputs.score }}."
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Leaf multiprocessing control
132 | .leaf.lock
133 | .*.lock
134 |
135 | # Visual Studio Code config files
136 | .vscode/
137 |
138 | # MacOS Annoying file
139 | .DS_Store
140 |
141 | # Local test env
142 | .test/
143 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | """Leaf 框架配置示例文件"""
2 |
3 | # 以下是系统常量, 请勿进行修改
4 |
5 | ###############################################################
6 |
7 | # 静态字典类
8 |
9 |
10 | class Static(dict):
11 | """静态 Dict 类"""
12 |
13 | def __getattr__(self, key):
14 | """返回本地键值对"""
15 | return self[key]
16 |
17 | def __setattr__(self, key, value):
18 | """删除设置功能"""
19 | raise RuntimeError("StaticDict cannot be set value")
20 |
21 | def __delattr__(self, key):
22 | """删除删除功能"""
23 | raise RuntimeError("StaticDict cannot be remove value")
24 |
25 |
26 | # 日志等级常量定义
27 | CRITICAL, FATAL, ERROR = 50, 50, 40
28 | WARNING, INFO, DEBUG, NOTSET = 30, 20, 10, 0
29 |
30 | ###############################################################
31 |
32 | # 配置文件开始, 您可以开始您的配置
33 |
34 | # 基础相关配置
35 | # domain - 您部署 Leaf 的域名
36 | # locker - 进程锁文件名 - 一般无需更改
37 | # manager - 管理器连接地址 - 一般无需更改
38 | # autkey - 管理器验证密钥 - 请更换成您喜欢的英文单词
39 | basic = Static({
40 | "domain": "wxleaf.dev",
41 | "locker": ".leaf.lock",
42 | "manager": None,
43 | "authkey": "password"
44 | })
45 |
46 |
47 | # 开发相关配置
48 | # 在生产环境请务必禁用开发者模式!
49 | # enable: 启用开发者模式 - 仅当为 True 时以下选项生效
50 | # 该选项同时影响 Flask 的默认 Debug 启动参数
51 | # token: 开发用 JWT Token - 无需验证身份
52 | devlopment = Static({
53 | "enable": False,
54 | "token": "here is a secret token"
55 | })
56 |
57 |
58 | # 错误与日志处理配置
59 | logging = Static({
60 |
61 | # 默认的日志等级; 一般不需要修改
62 | "level": INFO,
63 |
64 | # 日志的存储位置及名称
65 | # 请确保程序有该处的读写权限
66 | "rcfile": "leaf.log",
67 |
68 | # 日志格式 - None 表示使用默认; 更多配置请参考
69 | # https://docs.python.org/3/library/logging.html
70 | "format": None,
71 |
72 | # 控制台日志输出配置
73 | # level - 单独设置控制台的输出等级
74 | # format - 单独设置控制台的输出格式; None 表示继承上面的格式
75 | "console": Static({
76 | "level": DEBUG,
77 | "format": None # 表示使用父级 logger 配置
78 | }),
79 |
80 | # 文件日志输出配置
81 | # level - 单独设置文件的输出等级
82 | # format - 单独设置文件的输出格式; None 表示继承上面的格式
83 | "file": Static({
84 | "level": ERROR,
85 | "format": None # 表示使用父级 logger 配置
86 | })
87 | })
88 |
89 |
90 | # 微信公众平台配置
91 | # appid - 微信公众平台 AppId
92 | # aeskey - 微信公众平台 AESKey
93 | # token - 微信公众平台消息传递 Token
94 | # secret - 微信公众平台 AppSecret
95 | # accesstoken.enbale - 是否启用 ACToken 自动更新功能
96 | # accesstoken.retries - 遇到错误之后的重试次数
97 | weixin = Static({
98 | "appid": "wxabcd1234abcd1234",
99 | "aeskey": "s5d6t7vybotcre3465d68f7ybvtd4sd5687g8huhyvt",
100 | "token": "s547d6figobunb67568d8f7g8ohjiks1",
101 | "secret": "f5a3462707c2a31e51ff1b04efd1ed39",
102 | "accesstoken": Static({
103 | "enable": True,
104 | "retries": 5
105 | })
106 | })
107 |
108 |
109 | # 微信支付配置
110 | # appid - 微信支付关联的公众号 AppId
111 | # mchid - 微信支付商户 Id
112 | # apikey - 微信支付 API 密钥
113 | wxpay = Static({
114 | "appid": "wxabcd1234abcd1234",
115 | "mchid": "8888888888",
116 | "apikey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
117 |
118 | # 回调地址
119 | # pay - 表示支付成功的回调地址
120 | # refund - 表示退款成功的回调地址
121 | # 除非您有其余服务部署在以下地址, 否则请不要修改
122 | "callbacks": {
123 | "pay": "https://" + basic.domain + "/wxpay/notify",
124 | "refund": "https://" + basic.domain + "/wxpay/notify_refund"
125 | },
126 |
127 | # 退款操作证书位置
128 | # 第一个为签名文件路径, 第二个为证书文件路径
129 | # 请填写绝对路径
130 | # 如果您确定自己的不需要使用退款功能, 可以随意填写
131 | "cert": (
132 | "root\\key.pem",
133 | "root\\cert.pem"
134 | )
135 | })
136 |
137 |
138 | # 数据库配置
139 | # database - 需要连接的数据库
140 | # host - 数据库服务器地址
141 | # port - 数据库服务端口
142 | # username - 用户名; None 表示不需要
143 | # password - 密码; None 表示不需要
144 | # timeout - 连接超时时间; 以秒为单位
145 | database = Static({
146 | "database": "test",
147 | "host": "localhost",
148 | "port": 27017,
149 | "username": None,
150 | "password": None,
151 | "timeout": 5
152 | })
153 |
154 |
155 | # 插件配置
156 | # autorun - Leaf 启动时插件是否自动运行
157 | # directory - 插件加载位置; None 表示不需要
158 | plugins = Static({
159 | "autorun": False,
160 | "directory": None
161 | })
162 |
--------------------------------------------------------------------------------
/demo/hello/README.md:
--------------------------------------------------------------------------------
1 | 这将是您入门 `Leaf` 使用的第一步☝️,只需要简单的环境准备与配置,您就可以在自己的微信公众号中看到一个来自 `Leaf` 的问候,让我们开始吧😄!
2 |
3 | ## 环境准备
4 |
5 | 在运行这个 `Demo` 之前,您需要按照如下步骤准备环境:
6 |
7 | 1. 安装最新版本的 `Leaf`:
8 |
9 | ```shell
10 | pip install wxleaf
11 | ```
12 |
13 | 版本应在 `1.0.9.1.dev8` 及以上
14 |
15 | 2. 确保您当前用户有权限使用服务器的80端口
16 |
17 | 3. 在微信公众平台获取您微信公众账号的如下开发配置参数:
18 |
19 | - AppID
20 | - AppSecret
21 | - Token - 一般是您自己设置
22 | - AESEncryptKey - 如果您选择加密/混合模式发送消息
23 |
24 | ## 配置文件
25 |
26 | 之后在 `config.py` 的 第 98 - 101 行分别填入您刚刚获取到的开发参数:
27 |
28 | ```python
29 | weixin = Static({
30 | "appid": "wxabcd1234abcd1234", # AppID
31 | "aeskey": "s5d6t7vybotcre3465d68f7ybvtd4sd5687g8huhyvt", # AESEncryptKey
32 | "token": "s547d6figobunb67568d8f7g8ohjiks1", # Token
33 | "secret": "f5a3462707c2a31e51ff1b04efd1ed39", # AppSecret
34 | ...
35 | })
36 | ```
37 |
38 | ## 开始
39 |
40 | 运行我们的服务器:
41 |
42 | ```shell
43 | python3 run.py
44 | ```
45 |
46 | 所有配置都已经完成,在微信公众平台接口配置信息的 `URL` 部分填入您服务器的域名+ `/weixin/callback` 之后提交;
47 |
48 | 现在,在您的公众号发送一条消息,即可看到来自 `Leaf` 的问候!
49 |
50 | 直接访问您的域名也可以看到它:
51 |
52 | 
53 |
54 | Enjoy using! 😉
55 |
56 |
--------------------------------------------------------------------------------
/demo/hello/config.py:
--------------------------------------------------------------------------------
1 | """Leaf 框架配置示例文件"""
2 |
3 | # 以下是系统常量, 请勿进行修改
4 |
5 | ###############################################################
6 |
7 | # 静态字典类
8 |
9 |
10 | class Static(dict):
11 | """静态 Dict 类"""
12 |
13 | def __getattr__(self, key):
14 | """返回本地键值对"""
15 | return self[key]
16 |
17 | def __setattr__(self, key, value):
18 | """删除设置功能"""
19 | raise RuntimeError("StaticDict cannot be set value")
20 |
21 | def __delattr__(self, key):
22 | """删除删除功能"""
23 | raise RuntimeError("StaticDict cannot be remove value")
24 |
25 |
26 | # 日志等级常量定义
27 | CRITICAL, FATAL, ERROR = 50, 50, 40
28 | WARNING, INFO, DEBUG, NOTSET = 30, 20, 10, 0
29 |
30 | ###############################################################
31 |
32 | # 配置文件开始, 您可以开始您的配置
33 |
34 | # 基础相关配置
35 | # domain - 您部署 Leaf 的域名
36 | # locker - 进程锁文件名 - 一般无需更改
37 | # manager - 管理器连接地址 - 一般无需更改
38 | # autkey - 管理器验证密钥 - 请更换成您喜欢的英文单词
39 | basic = Static({
40 | "domain": "wxleaf.dev",
41 | "locker": ".leaf.lock",
42 | "manager": None,
43 | "authkey": "password"
44 | })
45 |
46 |
47 | # 开发相关配置
48 | # 在生产环境请务必禁用开发者模式!
49 | # enable: 启用开发者模式 - 仅当为 True 时以下选项生效
50 | # 该选项同时影响 Flask 的默认 Debug 启动参数
51 | # token: 开发用 JWT Token - 无需验证身份
52 | devlopment = Static({
53 | "enable": False,
54 | "token": "here is a secret token"
55 | })
56 |
57 |
58 | # 错误与日志处理配置
59 | logging = Static({
60 |
61 | # 默认的日志等级; 一般不需要修改
62 | "level": INFO,
63 |
64 | # 日志的存储位置及名称
65 | # 请确保程序有该处的读写权限
66 | "rcfile": "leaf.log",
67 |
68 | # 日志格式 - None 表示使用默认; 更多配置请参考
69 | # https://docs.python.org/3/library/logging.html
70 | "format": None,
71 |
72 | # 控制台日志输出配置
73 | # level - 单独设置控制台的输出等级
74 | # format - 单独设置控制台的输出格式; None 表示继承上面的格式
75 | "console": Static({
76 | "level": DEBUG,
77 | "format": None # 表示使用父级 logger 配置
78 | }),
79 |
80 | # 文件日志输出配置
81 | # level - 单独设置文件的输出等级
82 | # format - 单独设置文件的输出格式; None 表示继承上面的格式
83 | "file": Static({
84 | "level": ERROR,
85 | "format": None # 表示使用父级 logger 配置
86 | })
87 | })
88 |
89 |
90 | # 微信公众平台配置
91 | # appid - 微信公众平台 AppId
92 | # aeskey - 微信公众平台 AESKey
93 | # token - 微信公众平台消息传递 Token
94 | # secret - 微信公众平台 AppSecret
95 | # accesstoken.enbale - 是否启用 ACToken 自动更新功能
96 | # accesstoken.retries - 遇到错误之后的重试次数
97 | weixin = Static({
98 | "appid": "wxabcd1234abcd1234",
99 | "aeskey": "s5d6t7vybotcre3465d68f7ybvtd4sd5687g8huhyvt",
100 | "token": "s547d6figobunb67568d8f7g8ohjiks1",
101 | "secret": "f5a3462707c2a31e51ff1b04efd1ed39",
102 | "accesstoken": Static({
103 | "enable": True,
104 | "retries": 5
105 | })
106 | })
107 |
108 | # 插件配置
109 | # autorun - Leaf 启动时插件是否自动运行
110 | # directory - 插件加载位置; None 表示不需要
111 | plugins = Static({
112 | "autorun": True,
113 | "directory": None
114 | })
115 |
--------------------------------------------------------------------------------
/demo/hello/http.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/demo/hello/http.jpg
--------------------------------------------------------------------------------
/demo/hello/run.py:
--------------------------------------------------------------------------------
1 | """Hello Leaf"""
2 |
3 | import logging
4 | import leaf
5 | import flask
6 | import config
7 |
8 | # 初始化核心模块
9 | init = leaf.Init()
10 | init.kernel(config.basic)
11 | init.logging(config.logging)
12 | init.server(config.devlopment)
13 | # init.database(config.database)
14 |
15 | # 初始化插件与微信模块
16 | init.weixin(config.weixin) # 微信公众平台支持模块
17 | init.plugins(config.plugins) # 插件模块
18 |
19 | # 获取模块实例
20 | logger = logging.getLogger("leaf.demo.hello")
21 | server: flask.Flask = leaf.modules.server # 服务器模块
22 | message: leaf.weixin.reply.Message = leaf.modules.weixin.message # 微信公众平台消息处理模块实例
23 |
24 | # 处理文本类型的消息
25 | @message.register("text")
26 | def reply_hello(**kwargs):
27 | """返回 Hello Leaf"""
28 | content = kwargs.get("content")
29 | logger.debug("MessageReceived: " + content)
30 | return leaf.weixin.reply.Maker.text("Hello Leaf!")
31 |
32 | # 在主页也返回一个 Hello Leaf
33 | @server.route("/")
34 | def show_hello():
35 | """返回 Hello Leaf"""
36 | return "
Hello Leaf!
"
37 |
38 | server.run(host="0.0.0.0", port=80)
39 |
--------------------------------------------------------------------------------
/demo/hello/weixin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/demo/hello/weixin.jpg
--------------------------------------------------------------------------------
/docs/accesspoint.py:
--------------------------------------------------------------------------------
1 | """
2 | 接入点代码管理器:
3 | 管理现有的接入点
4 | 根据视图函数生成接入点信息
5 | """
6 |
7 | # 将 leaf 加入临时目录并引用
8 | import json
9 | import os as _os
10 | from typing import Optional
11 | from typing import Callable
12 | from typing import NoReturn
13 | from typing import List, Tuple
14 | from functools import namedtuple
15 | from collections import defaultdict
16 | import argparse as _argparse
17 |
18 | workdir: str = _os.path.dirname(_os.path.realpath(__file__))
19 | __import__("sys").path.append(_os.path.dirname(workdir))
20 |
21 | # pylint: disable=wrong-import-position
22 | import leaf as _leaf
23 |
24 | AccessRoute = namedtuple("AccessRoute", ("path", "methods"))
25 | AccessPoint = namedtuple("AccessPoint", ("name", "description"))
26 |
27 | Static = _leaf.core.algorithm.StaticDict
28 | CRITICAL, FATAL, ERROR = 50, 50, 40
29 | WARNING, INFO, DEBUG, NOTSET = 30, 20, 10, 0
30 |
31 | # 生成临时使用核心模块配置
32 | basic = Static({
33 | "domain": "accesspoint.manage",
34 | "locker": ".ap-testconfig-leaf.lock",
35 | "manager": None,
36 | "authkey": "password"
37 | })
38 |
39 | logging = Static({
40 | "level": INFO,
41 | "rcfile": ".hidden.leaf.log",
42 | "format": None,
43 | "console": Static({
44 | "level": DEBUG,
45 | "format": None
46 | }),
47 | "file": Static({
48 | "level": ERROR,
49 | "format": None
50 | })
51 | })
52 |
53 | # 生成 leaf 应用实例
54 | init = _leaf.Init()
55 | init.kernel(basic)
56 | init.logging(logging)
57 | application = init.server()
58 |
59 | # 获取所有的视图函数与接入点映射
60 | _functions = application.view_functions
61 | _paths = {ap.endpoint: AccessRoute(ap.rule, tuple(ap.methods)) for
62 | ap in application.url_map.iter_rules()}
63 | urlmap = {_paths[key]: _functions[key] for key in _functions.keys()}
64 | accesspoints = list()
65 |
66 | # markdown 表头
67 | __HEADER = \
68 | """
69 | 以下是 _Leaf_ 现有的所有接入点信息及其介绍:
70 |
71 | | 接入点代码 | 接入点描述 | 接入点路径 | 允许的请求方式 |
72 | | :------ | | :---- | | :----- | | :----------- |
73 | """
74 |
75 |
76 | # 命令行参数接收
77 | __parser = _argparse.ArgumentParser(description="接入点信息导出")
78 | __parser.add_argument("--type", "-t", default="markdown",
79 | help="type 表示导出文件类型, 支持 json, markdown. 默认为: markdown")
80 | __parser.add_argument("--export", "-e", help="export 参数表示导出的文件名", required=True)
81 |
82 |
83 | def __makeline(info: Tuple[str]) -> str:
84 | """制作新的一行"""
85 | return "| " + " | ".join(info) + " |"
86 |
87 |
88 | def __markdown(infos: List[Tuple[str]]) -> str:
89 | """制作 markdown 格式的数据"""
90 | lines = list()
91 | for info in infos:
92 | lines.append(__makeline(info))
93 | return __HEADER + '\n'.join(lines)
94 |
95 |
96 | def __json(infos: List[Tuple[str]]) -> str:
97 | """制作 JSON 格式的数据"""
98 | lines = defaultdict(list)
99 | for info in infos:
100 | lines[info[0]].append(info[1:4])
101 | return json.dumps(lines, indent=4, sort_keys=True, ensure_ascii=False)
102 |
103 |
104 | def unclosure(func: Callable, env: Optional[List[Tuple]] = None) -> \
105 | Tuple[Callable, List[Tuple]]:
106 | """
107 | 从闭包函数中迭代的找出最底层包装的函数
108 | 并逐层的统计闭包环境信息以列表返回
109 | """
110 | if env is None:
111 | env = list()
112 |
113 | # 如果是不是包底函数
114 | if "__closure__" in dir(func) and func.__closure__:
115 |
116 | # 被闭包的下一层函数
117 | next_stage: Callable = None
118 | stage_env = list()
119 |
120 | # 提取所有的闭包环境参数
121 | for envinfo in func.__closure__:
122 | content = envinfo.cell_contents
123 | if callable(content):
124 | next_stage = envinfo.cell_contents
125 | continue
126 | stage_env.append(content)
127 |
128 | env.append(tuple(stage_env))
129 | return unclosure(next_stage, env)
130 |
131 | # 如果是包底函数
132 | return func, env
133 |
134 |
135 | def doc(func: Callable) -> str:
136 | """处理底包函数的注释"""
137 | docs = func.__doc__.split('\n')
138 | first = docs[0] if docs[0] else docs[1]
139 | exhint = first.split('-')[0].strip()
140 | return exhint
141 |
142 |
143 | def pointname(env: List[Tuple]) -> str:
144 | """
145 | 从闭包的信息中匹配访问点名称
146 | 所有的接入点名称以 leaf. 开头
147 | 当未找到对应的接入点名称时返回 None
148 | """
149 | for layer in env:
150 | for info in layer:
151 | if isinstance(info, str) and info.startswith("leaf."):
152 | return info
153 | return None
154 |
155 |
156 | def reload() -> NoReturn:
157 | """加载所有的接入点信息"""
158 | # pylint: disable=global-statement
159 | global accesspoints
160 |
161 | for route, func in urlmap.items():
162 | bottom, env = unclosure(func)
163 | description = doc(bottom)
164 | name = pointname(env)
165 |
166 | # 跳过无接入点函数
167 | if name is None:
168 | continue
169 |
170 | accesspoints.append((
171 | name, description, route.path,
172 | ", ".join(route.methods)
173 | ))
174 |
175 | sorted(accesspoints, key=lambda item: item[0])
176 |
177 |
178 | def export(filename: str, _type: str) -> NoReturn:
179 | """导出文件"""
180 |
181 | content = ''
182 | if _type == "markdown":
183 | content = __markdown(accesspoints)
184 | if _type == "json":
185 | content = __json(accesspoints)
186 |
187 | with open(filename, 'w', encoding="utf-8") as handler:
188 | handler.write(content)
189 |
190 | # 完成导出, 切换出当前目录
191 | print("错误代码已导出到文件 '" + filename + "'")
192 |
193 |
194 | reload()
195 | if __name__ == "__main__":
196 | args = __parser.parse_args()
197 | export(args.export, args.type)
198 |
--------------------------------------------------------------------------------
/docs/accesspoints.md:
--------------------------------------------------------------------------------
1 |
2 | 以下是 _Leaf_ 现有的所有接入点信息及其介绍:
3 |
4 | | 接入点代码 | 接入点描述 | 接入点路径 | 允许的请求方式 |
5 | | :------ | | :---- | | :----- | | :----------- |
6 | | leaf.views.rbac.user.get | 批量获取用户信息 | /rbac/users | HEAD, OPTIONS, GET |
7 | | leaf.views.rbac.user.get | 根据用户 ID 查询用户 | /rbac/users/ | HEAD, OPTIONS, GET |
8 | | leaf.views.rbac.user.get | 根据用户 Index 查询用户 | /rbac/users// | HEAD, OPTIONS, GET |
9 | | leaf.views.rbac.user.update | 更新用户 informations 信息 | /rbac/users//informations | PUT, OPTIONS |
10 | | leaf.views.rbac.user.create | 创建一个用户的接口调用顺序如下: | /rbac/users | POST, OPTIONS |
11 | | leaf.views.rbac.user.update | 更新一个用户的状态 | /rbac/users//status | PUT, OPTIONS |
12 | | leaf.views.rbac.user.update | 将用户添加至用户组 | /rbac/users//groups/ | POST, OPTIONS |
13 | | leaf.views.rbac.user.update | 为指定用户增加一个索引信息 | /rbac/users//indexs | POST, OPTIONS |
14 | | leaf.views.rbac.user.update | 删除用户的一种指定索引 | /rbac/users//indexs/ | OPTIONS, DELETE |
15 | | leaf.views.rbac.user.delete | 删除某一个用户 | /rbac/users/ | OPTIONS, DELETE |
16 | | leaf.views.rbac.user.update | 将用户从组中移除 | /rbac/users//groups/ | OPTIONS, DELETE |
17 | | leaf.views.rbac.user.update | 更新用户头像 | /rbac/users/avatar/ | POST, OPTIONS |
18 | | leaf.views.rbac.user.update | 删除用户头像 | /rbac/users/avatar/ | OPTIONS, DELETE |
19 | | leaf.views.rbac.group.query | 根据给定的 id 查找用户组 | /rbac/groups/ | HEAD, OPTIONS, GET |
20 | | leaf.views.rbac.group.list | 列出所有的用户组信息 | /rbac/groups | HEAD, OPTIONS, GET |
21 | | leaf.views.rbac.group.query | 根据名称查找指定的用户组 | /rbac/groups/name/ | HEAD, OPTIONS, GET |
22 | | leaf.views.rbac.group.delete | 删除某一个特定的用户组 | /rbac/groups/ | OPTIONS, DELETE |
23 | | leaf.views.rbac.group.add | "增加一个用户组 | /rbac/groups | POST, OPTIONS |
24 | | leaf.views.rbac.group.update | 更新某一个用户组的信息 | /rbac/groups/ | PUT, OPTIONS |
25 | | leaf.views.rbac.group.edituser | 编辑用户组中的用户: | /rbac/groups//users | PUT, OPTIONS |
26 | | leaf.views.rbac.auth.create | 根据用户的某一个 index 创建 Auth 文档 | /rbac/auths// | POST, OPTIONS |
27 | | leaf.views.rbac.atuh.update | 更新用户的认证文档状态 | /rbac/auths//status | PUT, OPTIONS |
28 | | leaf.views.rbac.auth.delete | 删除用户的某一种认证方式 | /rbac/auths// | OPTIONS, DELETE |
29 | | leaf.views.rbac.auth.get | 查询用户的索引与认证文档的对应状态 | /rbac/auths/ | HEAD, OPTIONS, GET |
30 | | leaf.views.rbac.auth.update | 更新用户密码 | /rbac/auths//password | PUT, OPTIONS |
31 | | leaf.views.rbac.accesspoint.query | 根据指定的名称查找相关的访问点信息 | /rbac/accesspoints/ | HEAD, OPTIONS, GET |
32 | | leaf.views.rbac.accesspoint.get | 返回所有的访问点信息 | /rbac/accesspoints | HEAD, OPTIONS, GET |
33 | | leaf.views.rbac.accesspoint.delete | 删除某一个访问点信息 | /rbac/accesspoints/ | OPTIONS, DELETE |
34 | | leaf.views.rbac.accesspoint.create | 创建一个访问点信息 | /rbac/accesspoints | POST, OPTIONS |
35 | | leaf.views.rbac.accesspoint.update | 更新某一个访问点信息 | /rbac/accesspoints/ | PUT, OPTIONS |
36 | | leaf.views.rbac.accesspoint.update | 为指定的 AccessPoint 管理特权用户 | /rbac/accesspoints//exceptions | PUT, OPTIONS |
37 | | leaf.views.commodity.product.get | 批量获取产品信息 | /commodity/products | HEAD, OPTIONS, GET |
38 | | leaf.views.commodity.product.get | 根据产品ID查找产品 | /commodity/products/ | HEAD, OPTIONS, GET |
39 | | leaf.views.commodity.product.get | 根据产品名查找全部相关的的产品 | /commodity/products/name/ | HEAD, OPTIONS, GET |
40 | | leaf.views.commodity.product.get | 根据产品标签查找产品 | /commodity/products/tags/ | HEAD, OPTIONS, GET |
41 | | leaf.views.commodity.product.get | 查询全部的标签以及个数 | /commodity/products/tags | HEAD, OPTIONS, GET |
42 | | leaf.views.commodity.product.delete | 删除一个指定的产品 | /commodity/products/ | OPTIONS, DELETE |
43 | | leaf.views.commodity.product.update | 删除一个指定的产品参数 | /commodity/products//parameters/ | OPTIONS, DELETE |
44 | | leaf.views.commodity.product.update | 增加一个产品参数 | /commodity/products//parameters | POST, OPTIONS |
45 | | leaf.views.commodity.product.update | 更新产品在售状态 | /commodity/products//onsale | PUT, OPTIONS |
46 | | leaf.views.commodity.product.create | 创建一个产品 | /commodity/products | POST, OPTIONS |
47 | | leaf.views.commodity.product.update | 更新产品信息 | /commodity/products/ | PUT, OPTIONS |
48 | | leaf.views.commodity.product.get | 获取当前产品已经生成的所有商品列表 | /commodity/products//goods | HEAD, OPTIONS, GET |
49 | | leaf.views.commodity.product.update | 生成商品列表 | /commodity/products//goods | POST, OPTIONS |
50 | | leaf.views.commodity.product.update | 清除所有的生成商品 | /commodity/products//goods | OPTIONS, DELETE |
51 | | leaf.views.commodity.product.get | 查询所有的独立商品 | /commodity/goods | HEAD, OPTIONS, GET |
52 | | leaf.views.commodity.product.get | 根据标签查询所有的独立商品 | /commodity/goods/tags/ | HEAD, OPTIONS, GET |
53 | | leaf.views.commodity.product.get | 根据名称查询所有的独立商品 | /commodity/goods/name/ | HEAD, OPTIONS, GET |
54 | | leaf.views.commodity.product.update | 更新一个特定的产品信息 | /commodity/goods/ | PUT, OPTIONS |
55 | | leaf.views.commodity.product.create | 创建一个独立商品 | /commodity/goods | POST, OPTIONS |
56 | | leaf.views.commodity.product.update | 更改商品标签 | /commodity/goods//tags | PUT, OPTIONS |
57 | | leaf.views.commodity.product.update | 更新商品在售状态 | /commodity/goods//onsale | PUT, OPTIONS |
58 | | leaf.views.commodity.product.get | 查询一个指定的商品信息 | /commodity/goods/ | HEAD, OPTIONS, GET |
59 | | leaf.views.commodity.product.delete | 删除一个指定的商品 | /commodity/goods/ | OPTIONS, DELETE |
--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------
1 | # Leaf 的安装与部署
2 |
3 | ## 安装
4 |
5 | `Leaf` 目前提供三种安装方式:
6 |
7 | 1. 直接拷贝源代码
8 | 通过源代码安装只需要克隆本项目在 GitHub 上的源代码即可 - 通过这种方式安装的源代码需要拷贝到您的工作目录之中,但是它并不会添加到您的 `Python` 环境变量中:
9 |
10 | ```shell
11 | git clone https://github.com/guiqiqi/leaf
12 | cp leaf/leaf . -r
13 | ```
14 |
15 | 2. 通过源代码安装
16 | 通过源代码安装,步骤与上面类似,但是会添加到您的 `Python` 环境变量中:
17 |
18 | ```shell
19 | git clone https://github.com/guiqiqi/leaf
20 | cd leaf
21 | python setup.py install
22 | ```
23 |
24 | 3. 通过 Pypi 自动安装(推荐)
25 | 通过 `Pypi` 的发布包安装可能会比最新的代码落后一些版本,但是十分方便,您只需要执行:
26 | ```shell
27 | pip install wxleaf
28 | ```
29 |
30 | ## 部署
31 |
32 | `Leaf` 的部署十分简单,只需要三个步骤:
33 |
34 | 1. 创建一个您的项目文件夹,用于保存您项目相关的文件
35 | 2. 编写配置文件 - 可以直接参考项目 `config.py` 进行修改即可
36 | 3. 编写运行文件 - 参考 `run.py` 内的注释,选择自己需要加载的模块即可
37 |
38 | `Leaf` 基于 `Flask` 包,它的部署就如同任何一个普通的 `Flaks` 应用一样:当您只是想在本地调试代码时,可以直接启动 `run.py` 中自带的 `werkzeug` 的应用:
39 |
40 | ```Python
41 | server.run(host="localhost", port=80)
42 | ```
43 |
44 | 当需要在生产环境中运行时,推荐使用 `Gunicorn` 部署,示例命令:
45 |
46 | ```shell
47 | gunicorn -w 4 -b 0.0.0.0:80 run:server --daemon
48 | ```
49 |
50 | ***启动多个进程不会造成应用内的任务调度系统执行多次任务吗?***
51 | 您无需担心,我们已经在代码实现中解决了这个问题。
52 |
53 | 更多的参数配置请参考 `Gunicorn` 的[官方使用文档](https://docs.gunicorn.org/en/stable/)。
54 |
55 | 同时,我们也建议您在生产环境中使用 `Nginx` 作为代理转发,提升静态资源的访问性能。
56 |
--------------------------------------------------------------------------------
/docs/errcode.md:
--------------------------------------------------------------------------------
1 |
2 | 以下是 _Leaf_ 的现有的所有错误代码及其描述:
3 |
4 | | 错误代码 | 错误描述 | 所属模块 |
5 | | :----: | | :----: | :----: |
6 | | 10010 | 非法的 ObjectId 字符串 | leaf.api.error |
7 | | 10011 | 项目达到最大注册数量限制 | leaf.core.events |
8 | | 10012 | 非法根节点名称 | leaf.core.events |
9 | | 10013 | 非法事件名称 | leaf.core.events |
10 | | 10014 | 未找到对应的事件 | leaf.core.events |
11 | | 10015 | 无法找到该用户的Id验证文档 | leaf.rbac.error |
12 | | 10016 | 创建/更新身份验证-密码验证失败 | leaf.rbac.error |
13 | | 10017 | 根据给定的文档索引无法查找到身份验证文档 | leaf.rbac.error |
14 | | 10018 | 根据给定信息找不到用户 | leaf.rbac.error |
15 | | 10019 | 用户初始化已经完成 | leaf.rbac.error |
16 | | 10020 | 根据给定的信息找不到访问点文档 | leaf.rbac.error |
17 | | 10021 | 根据给定的信息找不到用户组文档 | leaf.rbac.error |
18 | | 10022 | 您所给定的用户索引信息已经被绑定 | leaf.rbac.error |
19 | | 10023 | 您所给定的用户索引类型已经被绑定 - 且当前策略不允许多绑定 | leaf.rbac.error |
20 | | 10024 | 不能删除根据 用户Id 创建的认证文档 | leaf.rbac.error |
21 | | 10025 | 根据给定信息找不到对应的产品 | leaf.selling.error |
22 | | 10026 | 找不到对应的产品参数信息 | leaf.selling.error |
23 | | 10027 | 产品参数信息发现重复 | leaf.selling.error |
24 | | 10028 | 根据给定信息找不到对应的商品 | leaf.selling.error |
25 | | 10029 | 不允许给定的货币类型进行交易 | leaf.selling.error |
26 | | 10030 | 不能用空商品列表创建订单 | leaf.selling.error |
27 | | 10031 | 商品货币类型不统一 | leaf.selling.error |
28 | | 10032 | 试图创建订单的商品已经停售 | leaf.selling.error |
29 | | 10033 | 所选商品库存不足 | leaf.selling.error |
30 | | 10034 | 无法找到支付平台通知到的订单 | leaf.selling.error |
31 | | 10101 | 插件载入时出错 | leaf.plugins.error |
32 | | 10102 | 没有找到对应的插件 | leaf.plugins.error |
33 | | 10103 | 插件 init 函数错误 | leaf.plugins.error |
34 | | 10104 | 插件运行期间出现错误 | leaf.plugins.error |
35 | | 11001 | 该状态码已经被使用, 请更换状态码 | leaf.core.algorithm.fsm |
36 | | 11002 | 当前状态不接受发生的指定事件 | leaf.core.algorithm.fsm |
37 | | 11003 | 根据当前状态和事件不能确定转移的目标状态 | leaf.core.algorithm.fsm |
38 | | 12001 | 对消息体的签名验证出现错误 | leaf.weixin.error |
39 | | 12002 | 在消息体加密过程中出现错误 | leaf.weixin.error |
40 | | 12003 | 在消息体解密过程中发生错误 | leaf.weixin.error |
41 | | 12004 | 消息体不正确(键缺少/数据类型非法) | leaf.weixin.error |
42 | | 12111 | JWT Token 头部格式错误/不支持 | leaf.rbac.error |
43 | | 12112 | JWT Token 格式错误 | leaf.rbac.error |
44 | | 12113 | JWT Token 的签名计算错误 - 检查secret是否与算法匹配 | leaf.rbac.error |
45 | | 12114 | JWT Token 签名验证错误 | leaf.rbac.error |
46 | | 12115 | JWT Token 过期 | leaf.rbac.error |
47 | | 12116 | 在 HTTP Header 信息中没有发现 JWT Token 信息 | leaf.rbac.error |
48 | | 13001 | 身份验证错误 | leaf.rbac.error |
49 | | 13003 | 传入的例外用户组无法被识别/用户ID错误 | leaf.rbac.error |
50 | | 13004 | 访问点名称冲突 | leaf.rbac.error |
51 | | 13005 | 未定义的用户索引类型 | leaf.rbac.error |
52 | | 14013 | 不正确的 APPID | leaf.weixin.accesstoken.error |
53 | | 14101 | 在获取过程中遇到网络错误 | leaf.weixin.accesstoken.error |
54 | | 14125 | 不正确的 AppSecret | leaf.weixin.accesstoken.error |
55 | | 14164 | 请在公众平台添加当前 IP 到白名单 | leaf.weixin.accesstoken.error |
56 | | 14202 | 获取到了不正确的响应 - 留意中间人攻击 | leaf.weixin.accesstoken.error |
57 | | 14203 | 经过多次重试后仍无法正常更新 AccessToken - 插件将停止运行 | leaf.weixin.accesstoken.error |
58 | | 14204 | 暂时没有缓存的 AccessToken | leaf.weixin.accesstoken.error |
59 | | 14205 | 缓存的 AccessToken 状态异常 | leaf.weixin.accesstoken.error |
--------------------------------------------------------------------------------
/docs/errcode.py:
--------------------------------------------------------------------------------
1 | """
2 | 错误代码管理器:
3 | 管理现有的错误代码与描述
4 | 生成错误代码描述文档
5 | """
6 |
7 | # 将文件路径向上转移
8 | import os as _os
9 | import json as _json
10 | import argparse as _argparse
11 | from collections import namedtuple as _namedtuple
12 | from collections import OrderedDict as _ODict
13 | from typing import List as _List
14 | from typing import Dict as _Dict
15 |
16 | workdir: str = _os.path.dirname(_os.path.realpath(__file__))
17 | __import__("sys").path.append(_os.path.dirname(workdir))
18 |
19 | # pylint: disable=wrong-import-position
20 | from leaf.core import error as _error
21 |
22 | # 设定错误信息存储类
23 | ErrorInfo = _namedtuple("ErrorInfo", ("description", "module"))
24 |
25 | # 获取 error.Error 的所有子类信息
26 | __informations: _Dict[int, str] = dict()
27 |
28 |
29 | def reload():
30 | """利用反射导出所有的错误信息"""
31 | __subclasses: _List[_error.Error] = _error.Error.__subclasses__()
32 | for _error_class in __subclasses:
33 | info = ErrorInfo(_error_class.description, _error_class.__module__)
34 | __informations[_error_class.code] = info
35 |
36 |
37 | # markdown 表头
38 | __HEADER = \
39 | """
40 | 以下是 _Leaf_ 的现有的所有错误代码及其描述:
41 |
42 | | 错误代码 | 错误描述 | 所属模块 |
43 | | :----: | | :----: | :----: |
44 | """
45 |
46 |
47 | # 命令行参数接收
48 | __parser = _argparse.ArgumentParser(description="错误代码导出")
49 | __parser.add_argument("--type", "-t", default="markdown",
50 | help="type 表示导出文件类型, 支持 json, markdown. 默认为: markdown")
51 | __parser.add_argument("--export", "-e", help="export 参数表示导出的文件名", required=True)
52 |
53 |
54 | def __makeline(key: int, info: ErrorInfo) -> str:
55 | """制作新的一行"""
56 | return "| " + str(key) + " | " + \
57 | info.description + " | " + info.module + " |"
58 |
59 |
60 | def __markdown(informations: _Dict[int, ErrorInfo]) -> str:
61 | """制作 markdown 格式的表格"""
62 | content_list = list()
63 | informations = _ODict(sorted(informations.items()))
64 | for key, value in informations.items():
65 | content_list.append(__makeline(key, value))
66 | return '\n'.join(content_list)
67 |
68 |
69 | def __json(informations: _Dict[int, ErrorInfo]) -> str:
70 | """制作 json 格式的数据"""
71 | errcodes: _Dict[int, _Dict[str, str]] = dict()
72 | for code, info in informations.items():
73 | errcodes[code] = dict(info._asdict())
74 |
75 | return _json.dumps(errcodes, indent=4, sort_keys=True, ensure_ascii=False)
76 |
77 |
78 | def export(filename: str, _type: str) -> _Dict[int, ErrorInfo]:
79 | """导出文件"""
80 |
81 | content = ''
82 | if _type == "markdown":
83 | content = __HEADER + __markdown(__informations)
84 | if _type == "json":
85 | content = __json(__informations)
86 |
87 | with open(filename, 'w', encoding="utf-8") as handler:
88 | handler.write(content)
89 |
90 | # 完成导出, 切换出当前目录
91 | print("错误代码已导出到文件 '" + filename + "'")
92 |
93 | return __informations
94 |
95 |
96 | reload()
97 | if __name__ == "__main__":
98 | args = __parser.parse_args()
99 | export(args.export, args.type)
100 |
--------------------------------------------------------------------------------
/docs/illustration/JWT验证.drawio:
--------------------------------------------------------------------------------
1 | 7Vxtc9o4EP41+hjG7y8fbSDt3NCZzNG5tp86Cgjw1SDGiAT6628lyxhbJjHEweaaTmZqrSVZ1u6zenbXCTL7y92nBK8XX+iUxMjQpjtkDpBh6IZjwX9csk8ltm+kgnkSTWWnXDCOfhMp1KR0G03JptCRURqzaF0UTuhqRSasIMNJQp+L3WY0Lj51jedEEYwnOFal36IpW6RSz3Bz+WcSzRfZk3XHT+8scdZZvslmgaf0+UhkDpHZTyhl6dVy1ycx37xsX9Jx9yfuHhaWkBWrMyDest1sRvb73w+jwfTpuz36Z3in2+k0TzjeyjdGQxsFA+S7aOiioI8CBw0tFHooFLc8D3mOuPC5JH01ts/2i0xh+2STJmxB53SF42EuDRO6XU0JX5QGrbzPiNI1CHUQ/ksY20tbwFtGQbRgy1jeJbuIfT+6/sGn6tmyNdjJmUVjnzVWLNl/zzvy5o/je/kw0crGTbbJk1grf5i641IJG7pNJuSlbZaWi5M5YS/0M9N+fBOPHiD1+YnQJYHFQQcJrzutp9u6lw5KSIxZ9FS0WyzNf34Ye5jugUbwGnkXOpttYHFHJgQXR0/NRcKwzjEyxcb++vYVBF/pL7JSLAhwsuaXsM84jklM5wleFi1gwxIYmuER3ihckySCZZKkPPAhvxE+LyJGxmssVPUM/qpgjXxinEyk2el81k16rfUM8yX1P5GEkV0dhVmOdAbSGxq2bD/nvkXXpGxx5FeycVX6LKjrXN2Y1gUQLqCiMTxrl+HZPQvQl2PYqIlhw68J4tqAfZOCDQV8g/Au2IK1nQLeFDO8YTQhJyBzpLoGIHGAgISE5VkKJOxrIkI31SOxvFVzMPp1/Zc/0BP8mM2gvbgptl7aFKvCT1gVu6Jr1jtti6fsyojiKWd2YC6qLT1HyxivuAnN6Ipl7kQcqYsono7wnm75mjcMT35lrXBBk+g39Me5p8cJk57DcPhsURz3aUy5o19R8YB80JhPJh+TEO69HzLt6CXRF7wrdByByWcLpHGM15so1RUfuATMR6uQMkaXJ/3Iy8b0Oh6kqk2npqbN9wKAr2g62mxAcMcfBo4DBpfVDe/GDudyST0VGsNxNF9BMyYzPoxvTgSUO5Bixg+OcANeJ1rNR6LPwMolf8tN4CIKY2exoNWwrinQCX4gMZwhjatqzYmO2CQ7hB/Ytj4/XmxYeB/aet6GH949YX26gnfBkdAtAdN4Jtw86mrdO1fpRk2lZ/2ad3qaonSyW0ulM2BQ/EDbraOEfCi/aeXbRsvKt1XlA5+WyhdH3c+YPPHA/kPzjWre9dqGvRqa9Xq9Dz03rOcDK2tP0Sp9E1mdexToaOggT0fBvbi4R74m0jtw4SiW0FZseI1cz0k+/2poKJNor4aGUglnpHcMPWOQ0pTudEnPupvuUeOnF9M9f16kcH6MbBXDQdNoPRy0FCV/JnjKWcPH2VEjw3tDoYGjaHoMusBsm5APZTes7PZDAUXZaZbnQ8+N6rl14m91h9rldO7H0Z3XqF2B2OU87wS1E62jUtBbSwF163lW3YLelWoBariHhj7yNOQHaOjxcMBzBfkPUCCiAAgKeLXXRcFQVIQdFJrIBy/hxBwfj3DeO3OBFJgnCJDX5/PwUboYNUDhkM8DMYU3UEyuWHx7pdaAHzcU3ooEeY0OFSp2FqqqDDZAv1y7wL4cV2VffgV4/fdK0Fp2d8B7UVzWLnjrRmtdA2/1pxoewM3iwIRoHeDGLwQAU9DxjzcOuIZbfeTf3yoMDbMYBdleyzg0OnSIXoJD/TwcXg65rLrweu3c7hbk3ArIORxCgS3PucDg55xvocAT6TMAmytuDQUsD2ehkmv7kyynYQ/u17Qm223amkq5tOyrHq3kmPRS1iVdqRyV2+RVk3Id8lVnEP43HPN1rSSbsSs+Ry2/j8kk4VahiaY2xsC9FQKepd/K8tPJmv/zh3alj4psq2YBxns3zm50B39nePzL8efUhV+3PpezKr7+qgxtwxD5pmADQ06rFXApyDmGlUTecjfnX833eEZrssAJ64kqxk/jBIYaAIZZItFuRTDrVADj3T63s9TKZEjgDcGBnagYXRs3l2PAcmuCoFtHUJaFLmBAUFrgsAAGX6SDAAOegUI/T+/whA8AQ2tdYxdxW2iUKeqlleSG+W5tM3Iat6Nqwuv4RSdilb9FPEF4myKzlhqX8RKz6qdvxSu7RnFDDcvv+cf/3HadtK0WAYUj6PPcMd9tR56KPux/mo0aIM8UfXzk36hHuHqexK5bV7C7lZrM1l36JYMHFX+t/IqBo5cS+BW/dWPbV4STc+u/dNNq+icz/ltL4NtVoYXiKasT+KLuxutx1g0n8E2veMo5pq3g8KoJfLuqpAJHWSjyu6Jc4vkfx9qbAVs3/d+1Y01NxbWu8TdooW4UYXRLCSrX7+PJoiKdeRm351TkEW9OMZEm3F4p5WJ5vko/Ktye/W70w2zdjC/6+OelCP1qzsypzdG7ldRxVI5+Yzg6fHeT4cipoPHN4Aia+R/mSLMW+Z83MYf/AQ==
--------------------------------------------------------------------------------
/docs/illustration/JWT验证.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/illustration/JWT验证.png
--------------------------------------------------------------------------------
/docs/illustration/事件管理器.drawio:
--------------------------------------------------------------------------------
1 | 7Vptb+I4EP41fGyVFxLgY4H2dk/tqtreaXc/GuISb02cOqaF/fU3jp13Q9MjBFWqhERmbE/smWc843EG7my9/YujOLxjAaYDxwq2A3c+cBzb8YfwJzk7xfEmjmKsOAl0p4LxQP5gzbQ0d0MCnFQ6CsaoIHGVuWRRhJeiwkOcs9dqt0dGq2+N0Qo3GA9LRJvcHyQQoeKOnVHB/4LJKszebPsT1bJGWWe9kiREAXstsdzrgTvjjAn1tN7OMJXKy/Sixt3sac0nxnEk2gxY//l7N/WefiXPvvs1GM1//3v3fKGlvCC60Qu+QxGsies5i12miOSVrCmKgJo+skg86BYL6GVIaHCLdmwjJ5IItHzKqGnIOPkD/RGFJhsY0MyFtrPjS2mE0hmjjAMjYukLikEPUph+DccJDLvPFmzXWHdoW+l4ixKRTZBRiuKELNIpy4FrxFckmjIh2Fp30prAXODtXhXbueEA8ZitseA76KIH5KjVYHcsTb8W0LF9zQvLsHHGGrIarqtcdmFReNBGfYeBnYaBB85U+suGq1VdAQFaFpukYXJQhEgtxtkTrpnIYDVEySoCkuJHOUxqkoAjXWm2YLEUFqMliVa3aZ/5sOB819qQLAZjH2nqLCEJAhxJozKYJFrkoIsZiUSqLW8KP1DqzLr0Bh5MfAa0XdDwk925mLEI1oJIakwM8HjFEiLtLL/ffZpwyMzvt7N+1q9z47t7jM/xkvEAnDy1/i0BLXjT6xe5Vm/+CYOOYeA5Z4aB14BBiKIAVJiqrLA7SLe+se9YbHhkAkIo1tkufipdjcbtdOVZJ9LVuLFqHEBCoElAT8hWLEL0uuACJjdRgAONyKLPLZNYT/X1Gwux01EPbQSTQa7QJt4S8bP0/EuKAtQqap7FtZTYZUQEy/1ZJkqjJFkMS6lsnFqfXNThIAc6YBu+xAd0pXMgCOgrfMjkntnkHFMkyEt1Hp0bdNQAvwL8seA+nAnUEwETru0+cT0xhIL9GqjuqunGiTmBmciYUfKC+4LbhdaGTov8yaA1f3LpnUhv2etKikPLJY4PAqge8xY6zwRXDK7koUTyKJOZbeH7l9aw4v7WcHJ4AwCipP9TbApvevYxu8fkyF1BD72XUC1BaFKH0KQqQs1Ljyofl+qChlVBdt01lXIaglKU5es5AnjNoxmcazb0aOCxOE2kApSEacyyVbMOTuO94WjidQ+wdiAx4uttbDr9AMz3e8WF+5mhtIdPa7C4o3PmKE7T1/tIUupQtkeGcNtrluL6HwXcXYLUabujnRekzVISbHFwQkewOsen8gi94PC0EsqlZRHxRiqtZxjn8DwbjJ3PPfod8Pc+BvybVZQM9aoe5VgLXVSDNv95I+v6sv6UPVYAmbfLE45X6VSAJmPqYbJWl/VXNsjRbGhSk7nQpW+s+uRU6YXKX/O1LFiwazB5gxNIHesss5C21G7flH6TjYRHOfgocapebXcv0jkoUjKbmjhWN2mkP8FavvUt8oTqOWyXD6weyTR5nOSrXSXjm4vype2/bX2+VFWXrR3F4FrlxnUMMXhsuvkaniwI+y2T6s/QvE+FrSPzWSu8brO2+Q9HUfJouMXuwdZdpkZtK+xnPr42a+xznIg9t8rdJv9+rUznmkrG/Z5hTbeualePjemd+hZBJWhWvM0zOMW/SC9MZduw1Ca3/wsdn9LETgemcnMgb3nB/Ez3kbjmlEQHsr9FxkjvhN0rvWHmMWlhiFNxnRc2otz/Wre6P5aNY9lozAug8WuUCBTJI6hlH5pVT3Nw+p7D5eWl+ZU1tzPfw9cu65u5Qfqtj9rqbEfTN2hNqPS3L5i+YCnVtAN2eX9mrEyZ7oUnJ/Pq4Tnzhiw5eFfeYJ8tb3Dbhi33rHmD9+FTQe8to6ZU/ZawywylpaWPvew7ztDN/GSmP3/rNzk5ZYEdyOJbUnXZVHyR617/Bw==
--------------------------------------------------------------------------------
/docs/illustration/事件管理器.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/illustration/事件管理器.png
--------------------------------------------------------------------------------
/docs/illustration/支付流程.drawio:
--------------------------------------------------------------------------------
1 | 7Vxtc6I6FP41+WgHCC/hIyi0e7ud7U53tu6nDltRuIPiRazaX38TiCKQAqKFuPd2Op0kBCHJ85zn5JxYAIfz7W3kLL2HcOIGQBImWwBHQJI0ScR/ScMubVB0PW2YRf4kbRKzhif/3aWNAm1d+xN3lesYh2EQ+8t842u4WLivca7NiaJwk+82DYP8U5fOzC01PL06Qbn12Z/EXtqKJC1rv3P9mbd/sqjS8c2dfWc6kpXnTMLNURO0ABxGYRinpfl26AZk7vbzkt5nf3D18GKRu4ib3GB5wsP9gzmO3+8Gj9H77cu36GWgpJ/y5gRrOmBgycA0ADKTgglMdQAsDegyMBCwVIAQMLXkGiKXLQUgI7mECwowIbB0oGvAGAELAcMChpRcGgGdPmsV7/aT607wXNNqGMVeOAsXTmBlrebrOnpzyQBEXInC9WKS1ARcy274GoZL2uVvN453FEXOOg5xkxfPA3oVz1S0G9P7k8ovUrlR9tXR9vjiaEdr5amms78K19ErHczj/Xr61/exJtw+P3sLH7rej5fBHrJONHPjin5y2o9MyNED6ELeuuHcxe+DO0Ru4MT+Wx6cDsX47NAvgwEuUCSwUfHo3U4NbfK++Loewx/udmA9oIHeFhV43UWy6HTp5Ytg4Pxl3/rx+Kh8tOi4lq05qeyOAdACKjnAXhY3rJVCveGm4q1zuCkBABcwVMy0BQE9sSEEUkYJG9nKk9nceH7sPi2dZOI2WG3yqzz1g2AYBmGU3AtNaNuqWrUIb24Uu9vKaaNXFWrCqYbJgpTWN5kiiHsz7x2pgSqcP9Hf7u6X09G9PXP++TLbuj9/yJ434MCUdsgpJsbb8UpqaI9FpU8eSQweqURSDUjIog+p1SXm16YWWUfn0ecCNJHlPE9EJHTHE6Z5FGEzJVOIYUIqKWBrZSYFZANdJPbLxLOs/QcJ14ZfsKFuQa50i4WSErtSL0cXqMur273zTUNanm9d0q3Ci63V/8JG4ZKyb9s6/rnQ/Kq86b7Ej/ALbeyQ2MYOXUT4lYbCL0t9GiLWdrws/DrAwop/yaUhMK3eDZEoFJiiCr0rP9uHwnYc23RJDfDzzd8RLs1IiekUtFCA3sh4IOCvY/4xydieQ2JTEjVVdwof4QZvsah2NOYZ/bTH0MfDyLqE0+kKv1sRPoeHtre9GgNQCkGDobYGlEJiJObBy+TIuucAJTaz7v1t61jIZC6iWgtESeUIiJWDPdVdhQyP1Qvnv9erTkRChXmN0MoSgRgKgT7LlxJZUc7TFKIkxNeiENwTWm4aqJFrCC2oSM7BbrDfJXFBcLaXz9pHnSY1ZWReidRU+y68IpPlgVZLzUC4kSWk5qGp8wTNytHnoNnADPasPagQwVf0vsXng5DTCeJTDqwgErlESvJBGinzSXGexEfbiV9+ym9P33ePm9lgpHhjO24eK6hjONShzhGj2UhkeZOniU0ZidgiaKRsJXjmIfN/jth0ErU6C4nV1g8jUURqqw22EUXO7qjDkuByxQDq4bxP3tBKcuFQSqG/As/rL0OpQIr0jT+TRlUrVRP6LlloywbmkJlL6HJnhgqLoHanjuzgHcsBZh5AQTowdDKdJp5jkebv0DDzQHgxPPyk7SrScbU+rcK0bX0dU2L7UMVoeQkkJap1HC3XC3TrP0ve+hRg+bxXEocihu2QN2/kQmgkIUiOE2pJ+MpmmcryR//P7QbcZvkQH+5XueE2C5ItzH3nmTBRz5NblHtn9xmnOQnhIPXiSUtt5vGqKNjKr29DQbUhBTWuKKg2cmkvS8FPPM0hQpW91+iPmlLZGfnzj3O0IZDWkED71eOEQayUMTMybxE/irQI5GsVvYtYkSmQsSHsWMSEtiKmkA02SkLR2GUkvWvPzV6ViHXmRyIGBysOL3NCQcT0I/EeEbuSBBvWfo/B11FqqXCUWmIkLDrWqvMzFg1MX9pHN5JNng0QP5u8VjmMzgRyD4VadjaN4BylKWUV5rB4BRl01heRLpBAL2BVTUJMxkVAe1XpdMaX3E7OrTdGcavcuoQUjWPQVo6+gNpaC8lFDkEsKpbc+/kufvLf3UUnTqXhx4ck8yz8OOfVgWNX9ZI1hyBrTz21cOxKpGDMcuPgv/iJjh2uZv9eILVS2f9ogNa/
--------------------------------------------------------------------------------
/docs/illustration/支付流程.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/illustration/支付流程.png
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | # Leaf - 开源微信商城系统框架
2 |
3 |     
4 |
5 | 
6 |
7 | `Leaf` 旨在实现一个对普通用户易用、对开发者友好的 *轻型* 开源 CMS 框架;`Leaf` 基于 `Python3.6+` 构建,后端使用 `Flask` 作为基础框架、`mongoengine` 进行数据库建模。
8 |
9 | 我们希望能减少普通用户搭建微信商城的成本,同时为有开发能力的朋友提供更多样化的功能。
10 |
11 | ---
12 |
13 | ## 特性介绍
14 |
15 | ### For Users
16 |
17 | `Leaf` 希望能够给没有开发能力的普通用户提供一个基础的、易于使用和管理的微信商城系统。
18 | 如果您是没有开发经验的普通用户,`Leaf` 可以提供给您:
19 |
20 | 1. **微信公众平台的接口使用能力**
21 | 即使您没有开发经验,也可以在我们的可视化后台中轻松地编辑多媒体文章、设置群发消息、管理用户/组、设置自动回复等等。
22 | 2. **主流支付接口的使用能力**
23 | 如果您有微信支付及支付宝支付等主流支付接口的使用权限,您可以在我们的后台中轻松配置支付方式,即时应用在您的交易当中。
24 | 3. **完善的用户/组/权限管理系统**
25 | `Leaf` 实现了基于角色的用户权限控制,通过在可视化后台中对用户的角色进行编辑,您可以轻松地配置多个用户组以分配工作 —— 管理员、编辑、库存管理员、甚至普通用户都可以进行分组。
26 | 4. **产品/库存/订单管理**
27 | `Leaf` 中集成了产品、商品库存、订单管理的功能,您可以轻松地管理 `SKU/SPU` 以及订单等信息。
28 | 5. **插件扩展能力**
29 | 您可以在我们的项目网站上寻找合适您需求的插件。
30 | 通过简单的单击按钮,您就可以启用或者禁用这些扩展,插件也可以使用上述的能力 —— 您可以使用插件设置定时任务,来进行微信推送、也可以使用插件来扩展商城功能,设置促销等等...
31 |
32 | ---
33 |
34 | ### For Developers
35 | 如果您有开发能力,`Leaf` 则能带给您更多可能,通过简单的二次开发,您可以使用到她的更多高级功能,下面是您可能感兴趣的部分:
36 |
37 | #### 功能特性
38 | 1. **任务计划支持**
39 | 不需要第三方的组件,您可以在开发过程中调用 `leaf.core.schedule` 模块,她可以帮助您实现 *轻量级* 的任务计划调度。
40 | 2. **事件机制**
41 | 您可以在代码的任何地方创建一个事件实例,通过 `events.hook` 方法可以将您的动作挂载到事件上,当事件发生时您的动作将会自动执行;在最近的版本中我们加入了多进程协作,您的事件甚至可以同步到远端的 `Leaf` 进程上执行。
42 | 3. **日志与错误系统**
43 | `Leaf` 基于 `logging` 包实现了较为完善的日志系统,同时定义的异常基类规范了系统中的异常使用。您只需要在您的代码中继承 `leaf.core.Error` 类,就可以在日志、API网关中得到详细的错误栈信息。
44 | 4. **权限控制**
45 | 正如上面在介绍所说,`Leaf` 实现了基于用户角色的权限控制,您也可以像系统应用一样定义自己的接入点,并通过简单的装饰器控制。
46 | 5. **插件系统**
47 | 插件系统是 `Leaf` 扩展性的重要保证。在 `Leaf` 中您可以:
48 | - **热插拔** 地管理插件,代码的更改变动可以仅通过一次插件重入得到部署;
49 | - 插件可以像在`Flask`的应用中设置路由一样,通过简单的装饰器控制视图路径、权限、访问IP等等
50 | - 插件可以调用系统中其余的资源(包括事件调度、日志、微信能力、数据库,甚至是其他插件的资源)
51 |
52 | #### 开发特性
53 | 1. **关于注释**
54 | 我们深知没有注释的代码等于天书这个道理,为了方便您的二次开发,`Leaf` 从核心模块到视图函数都有详细的注释。
55 | 2. **关于文档**
56 | 对于一些重要的系统功能,`Leaf` 会编写专门的文档进行说明 —— 例如如何开始您的插件开发,必要的地方还会配有插图,用于方便您的理解。
57 | 3. **类型提示**
58 | `Python` 从 3.5 版本之后支持了类型提示 `type hinting`,而我们则尽可能的在代码的各个部分使用这项全新功能,配合 `Visual Studio` 等 IDE,帮助您更轻松的开发。
59 | 4. **代码风格**
60 | `Leaf` 在开发过程我们尽可能的维持一致的代码风格,并且设置 `commit-Hook` 使用 `PyLint` 进行代码评分 —— 带目前为止,`Leaf` 获得的评分在 **9分** 以上,我们知道评分不能代表一切,但是仍希望能够做的更好。
61 | 5. **代码示例**
62 | >古人说: 纸上得来终觉浅,绝知此事要躬行。
63 |
64 | 我们会编写一些示例代码助于开发者的理解。
65 |
66 | **示例代码将会在 `demo` 文件夹中不断更新**,您也可以在 `Leaf` 的任意版本中找到示例插件(`Access Token` 中控插件)的代码,相信会更有助您的二次开发。
--------------------------------------------------------------------------------
/docs/logo-bar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/logo-bar.png
--------------------------------------------------------------------------------
/docs/other.md:
--------------------------------------------------------------------------------
1 | # 其他问题
2 |
3 | 这里说明一些关于 `Leaf` 的其他问题。
4 |
5 | 1. 项目的名称为什么有 `leaf` 和 `wxleaf` 两个版本呢?
6 |
7 | 项目开发时没有考虑到在 `Pypi` 上 *leaf* 这个名称已被占用,而项目与微信相关,于是便添加了 *wx* 作为前缀。所以您可以叫它 `wxleaf` ,在安装之后的调用使用 `leaf` 即可。
8 |
9 |
10 |
11 | 2. 项目为什么需要如此新的 `Python` 版本?
12 |
13 | 项目完全使用了 `TypeHinting` 的代码提示特性,而这个特性在 `Python3.6+` 的版本才被完全支持,所以我们才需要新的版本支持;
14 |
15 | 当然,我相信对大家来说安装 `Python3.6+` 不算难事,毕竟都2020年了。
16 |
17 |
18 |
19 | 3. 我可以去哪里反馈问题?
20 |
21 | 您可以到项目的 [GitHub 代码仓库](https://github.com/guiqiqi/leaf) 通过 Issue 的方式提交问题,记得查看 Issue 的模板,确保自己的提问符合要求,也方便我第一时间跟进。
--------------------------------------------------------------------------------
/docs/public/logo-bar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/public/logo-bar.png
--------------------------------------------------------------------------------
/docs/public/logo.old.drawio:
--------------------------------------------------------------------------------
1 | 5Zhbb5swFMc/TR5bcQlO8piQsE1q1UmZ1HUvkwMOuAXMjMlln37HYAIEkrRdUastL8F/X8/vHB8bBqYd7T5xnAS3zCPhwNC83cCcDwxDN9AQ/qSyV8oI6YXic+oprRKW9DdRoqbUjHokbTQUjIWCJk3RZXFMXNHQMOds22y2ZmFz1gT7pCUsXRy21XvqiaBQx8ao0j8T6gflzDqaFDURLhsrS9IAe2xbk8zFwLQ5Y6J4inY2CSW9kkvRzzlRe1gYJ7F4TofEoxa+17zsEXk/gtnsi/3TuSrt2OAwUxar1Yp9icDnLEvas6kFbAgXZNflC7wqR6jMhUAhLCKC76HdtgKqI0UpqMO0lIiVE/1D38pOeFCmvsBs/bLVMArEGBRm24AKskywK2u2EOegBSKCCec6POI0KQJvTXcEJp2taRjaLGQ8H8i0nfnUgcXO2gjPu+QYbBug2lRDhanG0+jAafRF07hM8y8ZOo5jT1EvDMvaEuK+Ceu9mJqXmcK2jD1JK0d4gfAR0MnCcebjPoGaw/cFqKP2lvYgtasi4yJgPotxuKjUmZvxzYFoxVeDUtXhhrFENXkkQuzVoYUzwZrMyY6K76q7fH6oPc939YIMOr3s8pVwCgwIL1vEwCMf59oqiw/lFLJQjZWX9qr0QtemLOMuOdNQARWY++TcgCq5StpnA4WTEAu6aR60b5+chq+Ig/5dr4/rvteukTZCE0u3htYYjUw0/kdiwfxYsfCanNB/LJjoWXmgkZzey6HGh3IounxK1hzRdUS2bh5wTU9kPU2Zna1kAKyw++TnUXCXiZDGpPtIhTuK4Rh9HqmoeUXR9fYdxUTtI3bY1xH7jHcHHrBolaUvvp84DtKnsz5hTpowTavNsuO6YvZ2h7Y+ZGr6T46p0YfKambXCxUKhfQdzIl8kaMqlDUDVPW4Qb8yVlZcpbnnp9BgZCQ7+Mt7anndVsW1rI0Zj3BYdZZtsNsc0GYRdaFiieMU/m6XjebuYfNWoolGeDVZ16Vi8TcEr0sDAFFhQ9MukNvGJiesb631JnOph6HGZnHKgEx9qRdZ6UPJ6pJxk/zXNu4uIbHEVAQnLEGSgsQCecXQ7okdYLlkHHt5qO8jUjgwSxLYwdeXsdQgHGUMSH2iucFTwdkTKRNrzOTh1ci1SkohMdPYB8GqSt/yLHKVB01X+maQbddh/k0roJ4HVh+nJBxSH9S5CzbK5DA7fATrvsOc+qZ0+rUddbynT97oWxIUq89zeV3tK6e5+AM=
--------------------------------------------------------------------------------
/docs/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/public/logo.png
--------------------------------------------------------------------------------
/docs/public/social-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/docs/public/social-page.png
--------------------------------------------------------------------------------
/leaf/api/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Leaf 框架 API 支持:
3 | wrap - 一个返回值 API 包装器
4 | settings - API 相关设置
5 | converter - 类型转换器注册表
6 | iplimit - 基于 IP 地址的访问过滤
7 | """
8 |
9 | from . import error
10 | from . import settings
11 | from . import validator
12 | from . import wrapper
13 |
--------------------------------------------------------------------------------
/leaf/api/error.py:
--------------------------------------------------------------------------------
1 | """API 错误定义"""
2 |
3 | from ..core import error as _error
4 |
5 |
6 | class InvalidObjectId(_error.Error):
7 | """非法的 ObjectId"""
8 | code = 10010
9 | description = "非法的 ObjectId 字符串"
10 |
--------------------------------------------------------------------------------
/leaf/api/settings.py:
--------------------------------------------------------------------------------
1 | """API 接口的设置文件"""
2 |
3 | from typing import Optional
4 | from flask import abort
5 | from ..core import error
6 |
7 |
8 | class Authorization:
9 | """权限验证中的设置"""
10 |
11 | ExecuteAPMissing = True # 在未找到接入点信息时是否允许
12 |
13 | @staticmethod
14 | def UnAuthorized(_reason: error.Error):
15 | """
16 | 验证失败时的返回值:
17 | _reason: 原因-错误类型
18 | """
19 | return abort(403)
20 |
21 | @staticmethod
22 | def NotPermitted(_diff: int, _strict: Optional[bool] = False):
23 | """
24 | 权限不足时的返回值:
25 | _diff: 所需权限与拥有权限的差值
26 | _strict: 是否指定需要某一级别权限值
27 | """
28 | return abort(403)
29 |
30 |
31 | class HTTPResponseHeader:
32 | """HTTP响应头部分的设置"""
33 | AddCORSSupport = True # 是否启用 CORS 请求支持
34 | CORSDomain = '*' # 启用支持 CORS 的域设置
35 | SupportMethods = [
36 | 'GET', 'HEAD', 'POST', 'PUT',
37 | 'DELETE', 'CONNECT', 'OPTIONS',
38 | 'TRACE', 'PATCH'
39 | ] # 支持的请求类型 - 如非必要请勿修改
40 |
41 |
42 | class Response:
43 | """响应中的设置"""
44 | Code = "code" # 错误代码键
45 | Description = "description" # 错误解释键
46 | Message = "message" # 错误消息键
47 |
48 | class Codes:
49 | """响应代码设置"""
50 | Success = 0 # 未发生错误的成功代码
51 | Unknown = -1 # 未知错误代码
52 |
53 | class Messages:
54 | """响应消息设置"""
55 | Success = "success" # 未发生错误时的成功消息
56 | Unknown = "undefined" # 未知错误消息
57 |
58 | class Descriptions:
59 | """响应解释设置"""
60 | Success = "成功" # 成功时的解释
61 | Unknown = "发生未知错误" # 未知错误解释
62 |
--------------------------------------------------------------------------------
/leaf/api/validator.py:
--------------------------------------------------------------------------------
1 | """API 函数工具库"""
2 |
3 | from typing import List, Optional
4 |
5 | import bson
6 | from flask import g as _g
7 | from flask import request as _request
8 |
9 | from . import error
10 | from .. import rbac
11 |
12 |
13 | def objectid(obj: str) -> bson.ObjectId:
14 | """
15 | 验证是否为合法的 ObjectId:
16 | 当传入的字符串为合法的 ObjectId 时返回 ObjectId 对象
17 | 否则掷出 error.InvalidObjectId 错误
18 | """
19 | try:
20 | return bson.ObjectId(obj)
21 | except bson.errors.InvalidId as _error:
22 | raise error.InvalidObjectId(obj)
23 |
24 |
25 | def operator(modified: str) -> bson.ObjectId:
26 | """
27 | 检查当前的操作者是否为指定对象(被修改对象):
28 | 当检查不通过时则掷出 rbac.error.AuthenticationError
29 | 当检查通过时候返回操作者的 userid: bson.ObjectId 对象
30 | """
31 | try:
32 | checkuser: bool = _g.checkuser
33 | _operator: str = _g.operator
34 | except AttributeError as _error:
35 | raise rbac.error.AuthenticationError(modified)
36 |
37 | # 当检查到操作者和被操作者不是同一用户时
38 | if checkuser and str(modified) != _operator:
39 | raise rbac.error.AuthenticationError(operator)
40 | return objectid(modified)
41 |
42 |
43 | def jwttoken(authorization: Optional[str] = None) -> dict:
44 | """
45 | 验证传入的 JWT Token 是否正确并返回 payload, 可能的错误:
46 | rbac.jwt.error.InvalidToken - Token 解析失败
47 | rbac.jwt.error.TokenNotFound - 未找到 Token
48 | rbac.jwt.error.InvalidHeader - 非法的头部信息
49 | rbac.jwt.error.TimeExired - Token 已经过期
50 | rbac.jwt.error.SignatureNotValid - 签名信息验证失败
51 | """
52 | # 获取 Bearer-Token
53 | if not authorization:
54 | authorization: str = _request.headers.get("Authorization")
55 |
56 | # 通过 Authorization 头获取 Token
57 | token: str = str()
58 | if not authorization:
59 | raise rbac.error.TokenNotFound()
60 | token = authorization.replace("Bearer", '', 1)
61 |
62 | # 验证 JWT Token 信息
63 | verification = rbac.jwt.Verify(token)
64 | verification.header()
65 | payload = verification.payload()
66 |
67 | # 通过 payload 获取用户盐并判断 Token 是否正确
68 | userid = payload.get(rbac.jwt.const.Payload.Audience)
69 | salt = rbac.functions.auth.Retrieve.saltbyindex(str(userid))
70 | verification.signature(salt)
71 |
72 | return payload
73 |
74 |
75 | def permission(pointname: str, payload: dict) -> int:
76 | """
77 | 验证 JWT Token 的权限是否足够访问对应接入点:
78 | pointname: 接入点名称
79 | payload: JWT Token 的 Payload 部分
80 |
81 | 返回的数值 diff: int 表示所需权限差:
82 | diff == 0 - 表示权限验证符合
83 | diff < 0 - 表示所需要权限不足(返回的差表示差多少)
84 | diff > 0 - 表示所需要权限足够但该访问点要求用户组对应
85 |
86 | 可能返回的错误:
87 | rbac.functions.error.AccessPointNotFound - 未找到对应的接入点信息
88 | bson.errors.InvalidId - 非法的 UserId 信息
89 | """
90 | # 获取权限点所需要的权限
91 | permitted: List[int] = payload.get(rbac.jwt.settings.Payload.Permission)
92 | accesspoint = rbac.functions.accesspoint.Retrieve.byname(pointname)
93 | if not accesspoint:
94 | raise rbac.error.AccessPointNotFound(pointname)
95 | userid = objectid(payload.get(rbac.jwt.const.Payload.Audience))
96 |
97 | # 验证是否是特权用户
98 | if userid in accesspoint.exceptions:
99 | return 0
100 |
101 | # 检查是否需要指定用户组
102 | if accesspoint.strict:
103 | if not accesspoint.required in permitted:
104 | return max(permitted) - accesspoint.required
105 | return 0
106 |
107 | # 检查权限是否足够
108 | if accesspoint.required > max(permitted):
109 | return max(permitted) - accesspoint.required
110 | return 0
111 |
--------------------------------------------------------------------------------
/leaf/core/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Leaf 框架核心部分:
3 | tools - 常用的工具函数库封装
4 | algorithm - 常用的数据结构算法封装
5 |
6 | error - 错误与日志记录
7 | events - 一个事件管理器
8 | wrapper - 常用的装饰器封装
9 | database - 提供一个连接池模块+MongoDB连接池
10 | schedule - 提供一个任务计划调度模块
11 |
12 | modules - 一个用于保存运行时实例的字典
13 | """
14 |
15 | from . import tools
16 | from . import abstract
17 | from . import algorithm
18 |
19 | # modules 保存 Leaf 框架运行时实例
20 | modules = algorithm.AttrDict()
21 |
22 | # pylint: disable=wrong-import-position
23 | from . import error
24 | from . import events
25 | from . import wrapper
26 | from . import database
27 | from . import schedule
28 | from . import parallel
29 |
--------------------------------------------------------------------------------
/leaf/core/abstract/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Leaf 抽象类集合:
3 | payment - 支付抽象类
4 | plugins - 插件抽象类
5 | """
6 |
7 | from . import payment
8 | from . import plugin
9 |
--------------------------------------------------------------------------------
/leaf/core/abstract/payment.py:
--------------------------------------------------------------------------------
1 | """Leaf 支付方式抽象类"""
2 |
3 |
4 | class AbstractPayment:
5 | """支付方式的抽象类"""
6 | description: str = str() # 这种支付方式的名称
7 |
8 | def __hash__(self) -> int:
9 | """支付类 Hash 支持"""
10 | return hash(self.description)
11 |
12 | def __str__(self) -> str:
13 | """支付类描述字符串"""
14 | return self.description
15 |
16 | def pay(self, orderid: str, amount: float, duration: int, describe: str,
17 | currency: str, details: dict, **kwargs) -> dict:
18 | """支付类的调用接口 - 抽象接口
19 | id: str 商户订单id
20 | details: dict 商品详细信息
21 | amount: float 总价金额
22 | describe: str 商品描述
23 | currency: str 订单币种
24 |
25 | 返回 预订单 的相关信息
26 |
27 | **kwargs 中可以添加自定义的一些参数
28 | """
29 |
30 | def query(self, orderid: str, **kwargs) -> dict:
31 | """支付类订单查询接口 - 抽象接口
32 | orderid: str 商户订单id/平台支付单id
33 |
34 | 返回 当前查询订单的相关信息
35 |
36 | **kwargs 中可以添加自定义的一些参数
37 | """
38 |
39 | def close(self, orderid: str, **kwargs) -> dict:
40 | """支付累订单关闭接口 - 抽象接口
41 | orderid: str 商户订单id/平台支付单id
42 |
43 | 返回关闭订单相关消息
44 |
45 | **kwargs 中可以添加自定义的一些参数
46 | """
47 |
48 | def refund(self, orderid: str, total_amount: float, refund_amount: float,
49 | refundid: str, currency: str, reason: str, **kwargs) -> dict:
50 | """支付类的退款接口 - 抽象接口
51 | orderid: str 商户订单id/平台支付单id
52 | amount: float 退款的金额
53 | refundid: str 退款id
54 | reason: str 退款原因
55 | cert: str 退款使用证书的绝对路径
56 |
57 | **kwargs 中可以添加自定义的一些参数
58 | """
59 |
60 | def query_refund(self, orderid: str, **kwargs) -> dict:
61 | """支付类的退款查询接口 - 抽象接口
62 | orderid: str 商户订单id/平台支付单id/商户退款单号/微信退款单号
63 |
64 | **kwargs 中可以添加自定义的一些参数
65 | """
66 |
--------------------------------------------------------------------------------
/leaf/core/algorithm/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 一些基础的数据结构/算法:
3 | tree: 一套完整的树结构/算法支持
4 | concurrent_dict: 键阻塞字典
5 | """
6 |
7 | from . import fsm
8 | from . import tree
9 | from . import keydict
10 | from . import concdict
11 |
12 | Node = tree.Node
13 | Tree = tree.Tree
14 | AttrDict = keydict.AttrDict
15 | StaticDict = keydict.StaticDict
16 | ConcurrentDict = concdict.ConcurrentDict
17 |
--------------------------------------------------------------------------------
/leaf/core/algorithm/concdict.py:
--------------------------------------------------------------------------------
1 | """一个线程安全的阻塞字典"""
2 |
3 | import threading
4 | from typing import Hashable, Dict, NoReturn, Optional
5 |
6 |
7 | class ConcurrentDict:
8 | """该字典当要pop的键不存在时, 会阻塞操作线程"""
9 |
10 | def __init__(self):
11 | self.__events: Dict[Hashable, threading.Event] = dict() # 用于存储键和查询锁对
12 | self.__data: Dict[Hashable, object] = dict() # 用于存储键值对
13 |
14 | def put(self, key: Hashable, value: object):
15 | """
16 | 向键值对中加入存储
17 | 该函数会检查 key 是否在 self.events 里
18 | 当存在时则说明有某些查询操作正在被阻塞, 此时需要对该
19 | 锁进行 set 操作, 通知对应的线程们取走数据
20 | """
21 | # 检验字典键是否可 hash 并存储值
22 | self.__data[key] = value
23 |
24 | # 设置事件 - 释放线程的阻塞状态
25 | if key in self.__events.keys():
26 | event = self.__events.pop(key)
27 | if not event.isSet():
28 | event.set()
29 |
30 | def get(self, key: Hashable, timeout: Optional[int] = None) -> object:
31 | """
32 | 从存储中返回值:
33 | 该函数会首先检查 self.data 中是否有对应的键
34 | 如果有则直接返回
35 | 否则在 self.events 中新建一个 threading.Event
36 | 对象并将其 clear, 之后进行 wait 操作阻塞线程
37 | 当 put 操作加入对应的键值对时, 该 event 对象
38 | 会被 set, 此时对应的阻塞释放, 返回相应的值
39 |
40 | *timeout: 超时时长, 超时之后触发 TimeoutError, 默认为无限大
41 | """
42 | # 检验字典键是否可 hash 并检查值是否已经存在
43 | # assert(isinstance(key, Hashable))
44 | if key in self.__data.keys():
45 | return self.__data.pop(key)
46 |
47 | # 判断是否已经有 Event 对象被创建, 如有则不创建
48 | event = threading.Event()
49 | event.clear()
50 | if not key in self.__events.keys():
51 | self.__events[key] = event
52 | else:
53 | event = self.__events[key]
54 |
55 | # 阻塞等待线程
56 | event.wait(timeout)
57 |
58 | # 当超时或者获取到之后
59 | if key in self.__data.keys():
60 | return self.__data[key]
61 | raise TimeoutError("key '%s' not found" % key)
62 |
63 | def erase(self, key: Hashable) -> NoReturn:
64 | """从存储中移除对应的键值对"""
65 |
66 | # assert(isinstance(key, Hashable))
67 | if key in self.__data.keys():
68 | self.__data.pop(key)
69 |
--------------------------------------------------------------------------------
/leaf/core/algorithm/keydict.py:
--------------------------------------------------------------------------------
1 | """实现一个支持通过 . 操作符访问键的字典类"""
2 |
3 | from typing import Hashable, NoReturn, Tuple
4 | # from multiprocessing.managers import BaseProxy
5 |
6 |
7 | class AttrDict(dict):
8 | """通过重写 __getattr__ 操作符使得字典支持 . 操作符访问"""
9 |
10 | _functions = {
11 | "keys", "fromkeys", "values", "setdefault", "update", "get",
12 | "clear", "popitem", "copy", "items", "pop"
13 | }
14 |
15 | def __setattr__(self, key: Hashable, value: object) -> NoReturn:
16 | """向字典的值和 attr 同时添加一份"""
17 | super().__setitem__(key, value)
18 | # super().__setattr__(key, value)
19 |
20 | def __getattr__(self, key: str) -> object:
21 | """重定向至 __getitem__"""
22 |
23 | # 当访问实例函数时返回函数对象
24 | if key in self._functions:
25 | return super().__getattribute__(key)
26 |
27 | return self[key]
28 |
29 | def __getstate__(self) -> Tuple[dict, dict]:
30 | """对象的 pickle 序列化封装函数"""
31 | return self.__dict__, dict(self)
32 |
33 | def __setstate__(self, state: Tuple[dict, dict]) -> NoReturn:
34 | """对象的序列化恢复"""
35 | self.__dict__.update(state[0])
36 | super().update(state[1])
37 |
38 |
39 | class StaticDict(AttrDict):
40 | """继承自 AttrDict 但是不允许修改数据"""
41 |
42 | def __setattr__(self, key: Hashable, value: object):
43 | """删除设置功能"""
44 | raise RuntimeError("StaticDict cannot be set value")
45 |
46 | def __delattr__(self, key: Hashable):
47 | """删除删除功能"""
48 | raise RuntimeError("StaticDict cannot be remove value")
49 |
--------------------------------------------------------------------------------
/leaf/core/database.py:
--------------------------------------------------------------------------------
1 | """Leaf 连接池实现"""
2 |
3 | import queue
4 | from typing import Callable, Optional, NoReturn
5 |
6 | # 自定义的数据库驱动模块
7 | import pymongo
8 |
9 | from . import wrapper
10 |
11 |
12 | class Pool:
13 | """一个通用连接池实现"""
14 |
15 | def __repr__(self) -> str:
16 | """返回repr信息"""
17 | return ""
18 |
19 | def __init__(self, size: int,
20 | creator: Callable[[], object],
21 | closer: Callable[[object], NoReturn],
22 | timeout: Optional[float] = 0):
23 | """
24 | 连接池初始化函数实现:
25 | size: 连接池默认空闲连接
26 | creator: 创建连接函数
27 | closer: 关闭连接函数
28 | timeout: 连接超时函数 - 默认为0: 不超时
29 | """
30 |
31 | # 通过 queue 来给出最空闲的连接
32 | self.__size = size
33 | self.__pool = queue.Queue(maxsize=size)
34 |
35 | # 构造创建函数 - 当不超时时
36 | if timeout <= 0:
37 | @wrapper.thread
38 | def _creator():
39 | self.__pool.put(creator())
40 | self.creator = _creator
41 |
42 | # 当设置超时 - 使用 timeout 计时器
43 | else:
44 | @wrapper.timelimit(timeout)
45 | def _creator():
46 | self.__pool.put(creator())
47 | self.creator = _creator
48 |
49 | # 构造关闭函数
50 | self.closer = closer
51 |
52 | # 初始化连接
53 | for _index in range(size):
54 | self.creator()
55 |
56 | def __del__(self):
57 | """析构时关闭所有链接"""
58 | self.stop()
59 |
60 | def stop(self) -> NoReturn:
61 | """
62 | 关闭所有链接
63 | """
64 | while self.__pool.qsize():
65 | self.closer(self.__pool.get())
66 |
67 | def get(self) -> object:
68 | """获取一个数据库连接"""
69 | connection = self.__pool.get()
70 | return connection
71 |
72 | def put(self, connection: object) -> NoReturn:
73 | """归还一个数据库连接"""
74 | self.__pool.put(connection)
75 |
76 | def status(self) -> float:
77 | """返回当前连接池的忙碌状态"""
78 | return 1 - self.__pool.qsize() / self.__size
79 |
80 |
81 | class MongoDBPool(Pool):
82 | """MongoDB 数据库连接池 - 继承自连接池"""
83 |
84 | @staticmethod
85 | def _create_connection(server: str, port: int, username: str,
86 | password: str, timeout: float, **optional) -> pymongo.MongoClient:
87 | """
88 | 创建一个 MongoDB 链接的静态函数:
89 | server: 数据库地址
90 | port: 数据库端口
91 | username: 用户名
92 | password: 密码
93 | timeout: 超时时间按照毫秒计算
94 | **optional: 可选项, 会被传递给 pymongo 驱动
95 | """
96 | return pymongo.MongoClient(
97 | server, port, username=username,
98 | password=password, **optional,
99 | serverSelectionTimeoutMS=timeout
100 | )
101 |
102 | @staticmethod
103 | def _close_connection(connection: pymongo.MongoClient) -> NoReturn:
104 | """关闭一个 MongoDB 的链接"""
105 | connection.close()
106 |
107 | def __init__(self, size: int, server: str, port: int,
108 | username: str, password: str, timeout: float, **optional):
109 | """
110 | 重写 Pool 的初始化函数
111 | *注意: timeout - 按照秒计算的超时时间
112 | *注意: database - 返回的直接选择的数据库
113 | *注意: **optional - 会被直接传递给 pymongo 驱动
114 | """
115 |
116 | # 创建连接与选择器函数包装
117 | def creater() -> pymongo.MongoClient:
118 | return self._create_connection(
119 | server, port, username, password,
120 | timeout * 1000, **optional)
121 |
122 | closer = self._close_connection
123 |
124 | # 初始化父类函数
125 | super().__init__(size, creater, closer, timeout)
126 |
127 | def version(self):
128 | """获取数据库版本"""
129 | connection = super().get()
130 | version = connection.server_info.get("version", "unknwon")
131 | super().put(connection)
132 | return version
133 |
134 | def get(self) -> pymongo.MongoClient:
135 | """获取一个数据库对象"""
136 | connection = super().get()
137 | return connection
138 |
139 |
140 | class MongoEnginePool(MongoDBPool):
141 | """使用 MongoEngine 管理链接不需要手动管理池"""
142 |
--------------------------------------------------------------------------------
/leaf/core/schedule.py:
--------------------------------------------------------------------------------
1 | """Leaf 任务计划与调度"""
2 |
3 | import uuid
4 | import time
5 | import threading
6 |
7 | from typing import Dict, Callable, Optional, Generator, NoReturn
8 |
9 | # 时间间隔常量定义
10 | MINUTE = 60 # 分钟间隔
11 | HOUR = MINUTE * 60 # 小时间隔
12 | DAY = HOUR * 24 # 每天间隔
13 | WEEK = DAY * 7 # 一周间隔
14 | MONTH = DAY * 30 # 每月间隔
15 |
16 |
17 | class Worker:
18 | """工作任务类"""
19 |
20 | def __repr__(self) -> str:
21 | """返回 repr 信息"""
22 | return ""
23 |
24 | def __init__(self, task: Callable[[], object], interval: float,
25 | runtimes: Optional[int] = 0, instantly: Optional[bool] = True,
26 | id_: Optional[str] = None,
27 | fineness: Optional[float] = 1):
28 | """
29 | 任务类初始化函数
30 | task: 需要运行的任务函数 - 需要经过包装(无参)
31 | interval: 每次运行的时间间隔
32 | instantly: 创建任务时是否立即运行
33 | runtimes: 可以指定任务运行指定次数后终止 - 默认为0 - 无限次
34 | id: 需要时可以手动指定该任务的id - 默认为自动生成
35 | fineness: 检查任务是否终止的细度 - 默认为1s
36 | """
37 | # 保存任务信息
38 | self.__task = task
39 | self.__interval = interval
40 | self.__fineness = fineness
41 | self.__instantly = instantly
42 |
43 | # 初始化任务变量
44 | self.__status = False # 任务是否在运行
45 | self.__runtimes = runtimes # 已经运行过的次数
46 | # self.__results = queue.Queue() # 任务返回值存储
47 | if id_ is None:
48 | self.__id = uuid.uuid1().hex
49 | else:
50 | self.__id = id_
51 |
52 | def _blocker(self) -> Generator:
53 | """
54 | 使用 yield 关键字实现协程间隔:
55 | 每次的阻塞过程分成许多个最小单位
56 | 每个最小单位的阻塞时间为 self.__fineness
57 | 之后检测是否已经阻塞足够长时间 + 是否被取消任务:
58 | 如果足够 - 运行任务
59 | 不够 - 继续休眠
60 | 被取消 - 终止
61 | """
62 | # 记录总的休眠时间
63 | gap: float = self.__interval
64 |
65 | while self.__status:
66 | # 如果休眠时长足够
67 | if gap <= 0:
68 | gap = self.__interval
69 | yield
70 |
71 | # 否则继续休眠
72 | time.sleep(self.__fineness)
73 | gap -= self.__fineness
74 |
75 | def _do(self) -> NoReturn:
76 | """
77 | 任务包装的运行函数:
78 | 通过循环 + 阻塞器实现
79 | 当任务次数达到之后设置运行标志位为 False
80 | 则会在下一次运行时退出循环
81 | """
82 | # 初始化阻塞器
83 | runtimes = self.__runtimes
84 | blocker = self._blocker()
85 |
86 | # 检测任务状态
87 | while self.__status:
88 |
89 | # 检测运行次数是否达到
90 | runtimes -= 1
91 | if runtimes == 0:
92 | self.stop()
93 |
94 | # 执行任务
95 | if not self.__instantly:
96 | self.__instantly = True
97 | runtimes += 1
98 | else:
99 | self.__task()
100 |
101 | # 进行任务休眠 - 休眠失败则退出执行
102 | try:
103 | blocker.send(None)
104 | except StopIteration as _error:
105 | break
106 |
107 | blocker.close()
108 |
109 | def start(self) -> NoReturn:
110 | """设置任务启动标志位并启动任务线程"""
111 | # 如果已经启动
112 | if self.__status is True:
113 | return
114 |
115 | # 如果还未启动 - 初始化线程并启动
116 | self.__status = True
117 | worker = threading.Thread(target=self._do)
118 | worker.setDaemon(True)
119 | worker.start()
120 |
121 | def stop(self) -> NoReturn:
122 | """设置任务终止标志位"""
123 | self.__status = False
124 |
125 | @property
126 | def interval(self) -> float:
127 | """返回任务间隔时长"""
128 | return self.__interval
129 |
130 | @interval.setter
131 | def interval(self, new: float) -> NoReturn:
132 | """重设任务间隔时长 - 下次运行时生效"""
133 | self.__interval = new
134 |
135 | @property
136 | def id(self) -> str:
137 | """返回任务id"""
138 | return self.__id
139 |
140 |
141 | class Manager:
142 | """通过字典来实现任务管理与记录"""
143 |
144 | def __repr__(self) -> str:
145 | """返回 repr 信息"""
146 | return ""
147 |
148 | def __init__(self, disable: Optional[bool] = False):
149 | """
150 | 通过保存一个字典来记录任务类
151 | disable 变量设置为 True 时不允许任何计划任务运行
152 | """
153 | self.__disable = disable
154 | self.__tasks: Dict[str, Worker] = dict()
155 |
156 | def get(self, id_: str) -> Worker:
157 | """根据任务 id 获取任务类"""
158 | return self.__tasks.get(id_)
159 |
160 | def start(self, worker: Worker) -> NoReturn:
161 | """新增一个工作类实例并启动该任务"""
162 | self.__tasks[worker.id] = worker
163 |
164 | # 检测到有其他进程运行任务时不运行任务
165 | if not self.__disable:
166 | worker.start()
167 |
--------------------------------------------------------------------------------
/leaf/core/tools/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 常用函数工具库封装:
3 | web - 网络功能库
4 | encrypt - 加密功能库
5 | file - 文件功能库
6 | time - 时间功能库
7 | """
8 |
9 | from . import web
10 | from . import time
11 | from . import file
12 | from . import encrypt
13 |
14 | web = web.WebTools
15 | time = time.TimeTools
16 | file = file.FileTools
17 | encrypt = encrypt.EncryptTools
18 |
--------------------------------------------------------------------------------
/leaf/core/tools/file.py:
--------------------------------------------------------------------------------
1 | """文件工具库"""
2 |
3 | import os
4 | import codecs
5 | import mimetypes
6 | import configparser
7 |
8 | from typing import List, IO, NoReturn, Tuple
9 |
10 | _DEFAULT_MIME = "application/octet-stream"
11 |
12 |
13 | class FileTools:
14 | """一些文件读取常用到的工具"""
15 |
16 | @staticmethod
17 | def isfile(source: str) -> bool:
18 | """判断某一文件是否存在
19 |
20 | *source: 文件的绝对/相对路径
21 | """
22 | return os.path.isfile(source)
23 |
24 | @staticmethod
25 | def isdir(source: str) -> bool:
26 | """判断某一目录是否存在
27 |
28 | *source: 文件夹的绝对/相对目录
29 | """
30 | return os.path.isdir(source)
31 |
32 | @staticmethod
33 | def dirs(location: str) -> List[str]:
34 | """获取某一路径下所有文件夹
35 |
36 | *location : 文件夹地址
37 | """
38 | location = location.strip("\\")
39 | dirs = os.listdir(location)
40 | for name in dirs[:]:
41 | full = location + "/" + name
42 | if not os.path.isdir(full):
43 | dirs.remove(name)
44 | return dirs
45 |
46 | @staticmethod
47 | def files(location: str) -> List[str]:
48 | """获取某一文件夹下的所有文件
49 |
50 | * location : 文件夹地址
51 | """
52 | location = location.strip("\\")
53 | files = os.listdir(location)
54 | for name in files[:]:
55 | full = location + "/" + name
56 | if os.path.isdir(full):
57 | files.remove(name)
58 | return files
59 |
60 | @staticmethod
61 | def read(location: str) -> IO[str]:
62 | """以UTF8编码读方式打开文件"""
63 | handler = codecs.open(location, "r", "utf-8")
64 | return handler
65 |
66 | @staticmethod
67 | def write(location: str) -> IO[str]:
68 | """以UTF8编码写方式打开文件"""
69 | handler = codecs.open(location, "w", "utf-8")
70 | return handler
71 |
72 | @staticmethod
73 | def mimetype(location: str) -> str:
74 | """根据文件名获取相应的mimetype"""
75 |
76 | # 默认为 数据流 模式
77 | default = _DEFAULT_MIME
78 | mimetype = mimetypes.guess_type(location)[0]
79 |
80 | # 当给不出尝试猜测的值时
81 | if not mimetype:
82 | mimetype = default
83 | return mimetype
84 |
85 | @staticmethod
86 | def read_config(handler: IO[str]) -> dict:
87 | """读取配置文件并返回字典"""
88 | # 生成 config_reader 类
89 | config_reader = configparser.ConfigParser()
90 | config_reader.read_file(handler)
91 | sections = config_reader.sections()
92 | config = dict()
93 |
94 | # 选中对应的 section 进行添加
95 | for section in sections:
96 | options = config_reader.options(section)
97 | config[section] = dict()
98 | # 选中 option 添加
99 | for option in options:
100 | value = config_reader.get(section, option)
101 | config[section][option] = value
102 |
103 | return config
104 |
105 | @staticmethod
106 | def write_config(handler: IO[str], config: dict) -> NoReturn:
107 | """将制定配置写入配置文件"""
108 |
109 | sections = config.keys()
110 | config_writer = configparser.ConfigParser()
111 |
112 | # 选中 section
113 | for section in sections:
114 | options = config[section].keys()
115 | config_writer.add_section(section)
116 | # 选中 option
117 | for option in options:
118 | value = config[section][option]
119 | config_writer.set(section, option, value)
120 |
121 | # 写入配置
122 | config_writer.write(handler)
123 |
124 | @staticmethod
125 | def edit_config(handler: IO[str], change: Tuple[str, str, str]) -> NoReturn:
126 | """按照配置修改文件"""
127 |
128 | # 创建 configparser 实例
129 | config_editor = configparser.ConfigParser()
130 | config_editor.read(handler)
131 |
132 | section = change[0] # 要更改的section
133 | option = change[1] # 要更改的option
134 | value = change[2] # 要更改的value
135 |
136 | # 如果没有有对应的 section 则创建一个
137 | if not config_editor.has_section(section):
138 | config_editor.add_section(section)
139 | config_editor.set(section, option, value)
140 |
141 | # 写入配置
142 | config_editor.write(handler)
143 |
--------------------------------------------------------------------------------
/leaf/core/tools/time.py:
--------------------------------------------------------------------------------
1 | """时间工具库"""
2 |
3 | import time
4 | import datetime
5 | from typing import Optional
6 | from collections import defaultdict
7 |
8 |
9 | class TimeTools:
10 | """获取时间信息的一些函数"""
11 |
12 | @staticmethod
13 | def now(resolution: Optional[int] = 1) -> object:
14 | """
15 | 获取当前时间戳
16 |
17 | *resolution:
18 | 时间精度, 为 10 的 n - 1 次方, 每秒产生个数
19 | 当传入 0 时不产生数据
20 | """
21 | now = int(time.time() * resolution)
22 | return now
23 |
24 | @staticmethod
25 | def stamp(time_s: str,
26 | format_: Optional[str] = "%Y-%m-%d %H:%M:%S") -> object:
27 | """根据给定时间字符串获取时间戳"""
28 |
29 | # 调用 datetime 库
30 | time_ = datetime.datetime.strptime(time_s, format_)
31 | time_tuple = time_.timetuple()
32 | time_stamp = int(time.mktime(time_tuple))
33 | return time_stamp
34 |
35 | @staticmethod
36 | def timestr(time_stamp: object,
37 | format_: Optional[str] = "%Y-%m-%d %H:%M:%S") -> str:
38 | """根据给定时间戳获取时间字符串"""
39 |
40 | # 调用 time 库
41 | time_stamp = int(time_stamp)
42 | time_tuple = time.localtime(time_stamp)
43 | time_string = time.strftime(format_, time_tuple)
44 | return time_string
45 |
46 | @staticmethod
47 | def nowstr(format_: Optional[str] = "%Y-%m-%d %H:%M:%S") -> str:
48 | """直接获取关于时间的字符串"""
49 | return datetime.datetime.utcnow().strftime(format_)
50 |
51 | @staticmethod
52 | def timediff(diff: int, unit: Optional[str] = "hours", resolution: Optional[str] = 0,
53 | fmt: Optional[str] = "{hours}:{minutes}:{seconds}") -> str:
54 | """
55 | 格式化时间差的文字:
56 | diff - 时间差的整数
57 | unit - 时间格式化的最大单位:
58 | "hours" - 格式化为小时, ...
59 | resolution - 输入时间差整数的精度:
60 | 0 - 秒单位, 1 - 0.1 秒单位, 2 - 0.01 秒, ...
61 | format_ - 要格式化的字符串格式:
62 | 使用按照关键字进行格式化的方式,
63 | 支持的有: years, months, days, hours,
64 | minutes, seconds, miliseconds
65 | 例如: {hours}:{minutes}:{seconds}
66 |
67 | *注意: 选则低于自己的时间差范围外的单位而格式化
68 | 字符串中没有对应键时数据会被丢弃, 这可能会造成错误
69 | timediff(60 * 60 * 25, "days", fmt="{hours}") -> '1'
70 | """
71 | # 将时间差转换为毫秒格式
72 | diff = diff * (10 ** (3 - resolution))
73 | remain, flag = diff, False
74 |
75 | # 计算年月日...
76 | callfuncs = (
77 | ("years", lambda remain: divmod(remain, 365 * 24 * 60 * 60 * 1010)),
78 | ("months", lambda remain: divmod(remain, 12 * 30 * 24 * 60 * 60 * 1000)),
79 | ("days", lambda remain: divmod(remain, 24 * 60 * 60 * 1000)),
80 | ("hours", lambda remain: divmod(remain, 60 * 60 * 1000)),
81 | ("minutes", lambda remain: divmod(remain, 60 * 1000)),
82 | ("seconds", lambda remain: divmod(remain, 1000)),
83 | ("miliseconds", lambda remain: (remain, 0)),
84 | )
85 |
86 | values = defaultdict(int)
87 |
88 | for key, callfunc in callfuncs:
89 |
90 | # 当单位没有到达需要的格式化范围时不进行操作
91 | if flag or key == unit:
92 | value, remain = callfunc(remain)
93 | values[key], flag = int(value), True
94 |
95 | return fmt.format_map(values)
96 |
--------------------------------------------------------------------------------
/leaf/core/wrapper.py:
--------------------------------------------------------------------------------
1 | """Leaf 装饰器函数库"""
2 |
3 | import time
4 | import queue
5 | import threading
6 |
7 | from typing import Callable, NoReturn
8 |
9 |
10 | def thread(function: Callable) -> object:
11 | """制造一个新线程执行指定任务"""
12 |
13 | def params(*args, **kwargs):
14 | """接受任务函数的参数"""
15 |
16 | # 通过线程执行函数
17 | def process(*args, **kwargs):
18 | """过程函数包装"""
19 | function(*args, **kwargs)
20 |
21 | _thread = threading.Thread(
22 | target=process, args=args, kwargs=kwargs)
23 | _thread.setDaemon(True)
24 | _thread.start()
25 |
26 | return params
27 |
28 |
29 | def timer(function: Callable) -> object:
30 | """计时函数 - 执行之后显示执行时间"""
31 |
32 | def wrapper(*arg, **kwargs):
33 | """参数接收器"""
34 | # 计时并执行函数
35 | start = time.time()
36 | result = function(*arg, **kwargs)
37 | end = time.time()
38 |
39 | # 显示时间
40 | used = (end - start) * 1000
41 | print("-> elapsed time: %.2f ms" % used)
42 | return result
43 |
44 | return wrapper
45 |
46 |
47 | def timelimit(limited: float) -> object:
48 | """
49 | 限制一个函数的执行时间:
50 | 1. 创建两个线程 - 计数器 + 工作线程
51 | 2. 通过一个 threading.Lock 的锁同步工作状态
52 | 3. 如果锁释放了则判断工作是否完成
53 |
54 | *注意: 这种方法在超时之后会触发 TimeoutError
55 | *注意: 但是并不会影响工作线程的工作 - 工作线程无法被动结束
56 | """
57 |
58 | def wrapper(function: Callable):
59 | """函数包装器"""
60 | # 初始化锁, 队列变量
61 | result = queue.Queue(maxsize=1)
62 | mutex = threading.Lock()
63 | mutex.acquire()
64 |
65 | def _timer_work() -> NoReturn:
66 | """需要计时器到时之后释放锁"""
67 | mutex.release()
68 |
69 | def params(*args, **kwargs):
70 | """参数接收器"""
71 |
72 | def _worker_work(*args, **kwargs):
73 | """任务工作线程"""
74 | result.put(function(*args, **kwargs))
75 | # 检查并尝试释放锁
76 | # pylint: disable=no-member
77 | if mutex.locked():
78 | mutex.release()
79 |
80 | # 设置定时器 + 工作线程
81 | _timer = threading.Timer(limited, _timer_work)
82 | _worker = threading.Thread(
83 | target=_worker_work, args=args, kwargs=kwargs)
84 | _worker.setDaemon(True)
85 | _worker.start()
86 | _timer.start()
87 |
88 | # 尝试获取锁变量之后检查任务状态
89 | if mutex.acquire():
90 | _timer.cancel()
91 |
92 | # 如果任务已经完成 - 返回结果
93 | if not result.empty():
94 | return result.get()
95 | # 如果任务未完成 - 触发超时
96 | raise TimeoutError
97 |
98 | return result.get_nowait()
99 |
100 | return params
101 | return wrapper
102 |
--------------------------------------------------------------------------------
/leaf/files/static/README.md:
--------------------------------------------------------------------------------
1 | ### 静态文件将会保存至此
--------------------------------------------------------------------------------
/leaf/files/uploads/README.md:
--------------------------------------------------------------------------------
1 | ### 用户上传的文件将会保存至此
--------------------------------------------------------------------------------
/leaf/payments/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 支付方式集合:
3 | wxpay - 符合 leaf.payment 抽象的微信支付模块
4 | alipay - 符合 leaf.payment 抽象的支付宝支付模块
5 | """
6 |
7 | from . import wxpay
8 | from . import alipay
9 | from . import yandex
10 |
--------------------------------------------------------------------------------
/leaf/payments/alipay/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 支付宝支付模块
3 | """
4 |
--------------------------------------------------------------------------------
/leaf/payments/wxpay/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信支付模块:
3 | payment - 主要支付模块
4 | signature - 微信支付签名工具
5 | settings - 微信支付相关设置
6 | methods - 微信支付方式
7 | const - 微信支付常量描述
8 | """
9 |
10 | from . import payment
11 | from . import methods
12 | from . import signature
13 |
14 | from . import const
15 | from . import settings
16 |
17 | signature = signature.SignatureTool
18 | methods = methods.WXPaymentType
19 | payment = payment.WXPayment
20 |
--------------------------------------------------------------------------------
/leaf/payments/wxpay/const.py:
--------------------------------------------------------------------------------
1 | """微信支付常量"""
2 |
3 |
4 | class WXPayAddress:
5 | """微信支付地址相关常量"""
6 | XMLTag = "xml" # 在 xml数据 外部包裹的xml标签
7 | Order = "https://api.mch.weixin.qq.com/pay/unifiedorder"
8 | Query = "https://api.mch.weixin.qq.com/pay/orderquery"
9 | Close = "https://api.mch.weixin.qq.com/pay/closeorder"
10 | Refund = "https://api.mch.weixin.qq.com/secapi/pay/refund"
11 | RefundQuery = "https://api.mch.weixin.qq.com/pay/refundquery"
12 |
13 |
14 | class WXPaySignature:
15 | """微信支付签名常量"""
16 | class Value:
17 | """固定值"""
18 | Type = "MD5" # 使用 MD5 方式签名
19 | Version = "1.0" # 固定值 1.0
20 |
21 | class Key:
22 | """固定键"""
23 | ApiKey = "key" # Apikey
24 | Version = "version" # 签名版本
25 | Sign = "sign" # 签名
26 | Type = "sign_type" # 签名类型
27 | Nonce = "nonce_str" # 随机字符
28 |
29 |
30 | class WXPayBasic:
31 | """微信支付基础常量"""
32 | AppID = "appid" # appid键
33 | Mch = "mch_id" # 商户号键
34 | OpenID = "openid" # OpenID
35 | TransactionID = "transaction_id" # 微信支付交易ID
36 |
37 |
38 | class WXPaymentNotify:
39 | """支付结果通知常量"""
40 |
41 | Status = "result_code"
42 |
43 | Fee = "cash_fee" # 支付金额
44 | FeeType = "cash_fee_type" # 支付币种
45 |
46 | class Error:
47 | """支付失败"""
48 | Code = "err_code" # 错误代码键
49 | Description = "err_code_des" # 错误描述
50 |
51 |
52 | class WXPayResponse:
53 | """微信支付响应常量"""
54 | Status = "return_code" # 通讯状态
55 | Message = "return_msg" # 通讯消息
56 | Success = "SUCCESS" # 通讯成功
57 | Fail = "FAIL" # 通讯失败
58 | Nothing = "OK" # 回复消息
59 |
60 |
61 | class WXPayOrder:
62 | """微信支付订单常量"""
63 | Callback = "notify_url" # 支付结果通知键
64 |
65 | class Type:
66 | """交易类型信息"""
67 | InApp = "APP" # APP 内部
68 | ScanQR = "NATIVE" # 原生支付
69 | JSAPI = "JSAPI" # JSAPI 支付
70 |
71 | class GoodsDetail:
72 | """详细信息内部键"""
73 | Name = "goods_name" # 商品名称
74 | Quantity = "quantity" # 商品数量
75 | Key = "goods_detail" # 外层包裹键
76 |
77 | class Info:
78 | """订单信息"""
79 | Describe = "body" # 订单描述
80 | Detail = "detail" # 详细信息
81 | Type = "trade_type" # 交易类型 - JSAPI...
82 | Attach = "attach" # 附加信息
83 | ProductID = "product_id" # 商品id
84 |
85 | class Time:
86 | """订单时间信息"""
87 | Start = "time_start" # 开始时间
88 | End = "time_expire" # 过期时间
89 | Format = "%Y%m%d%H%M%S" # 时间格式化模板
90 | Accuracy = 1000 # 时间生成器精度
91 |
92 | class Device:
93 | """支付设备信息"""
94 | IP = "spbill_create_ip" # 发起支付设备IP
95 | ID = "device_info" # 发起支付设备ID
96 |
97 | class Fee:
98 | """支付金额常量"""
99 | Amount = "total_fee" # 订单总金额
100 | Currency = "fee_type" # 订单支付币种
101 |
102 | class ID:
103 | """Id 相关常量"""
104 | In = "out_trade_no" # 商户的订单 Id
105 | Out = "transaction_id" # 微信的 trasaction_id
106 | Prepay = "prepay_id" # jsapi 调用返回的 prepay_id
107 |
108 |
109 | class WXPayRefund:
110 | """退款相关常量"""
111 | Offset = "offset" # 从第几个开始查询
112 | CurrentRefund = "refund_count" # 当前退款单数
113 | TotalRefunds = "total_refund_count" # 总共退款单数
114 |
115 | class Result:
116 | """退款结果信息"""
117 | @staticmethod
118 | def Status(no):
119 | """第几单的退款状态"""
120 | return "refund_status_" + str(no)
121 |
122 | @staticmethod
123 | def Account(no):
124 | """第几单的退款去向"""
125 | return "refund_recv_account_" + str(no)
126 |
127 | @staticmethod
128 | def Time(no):
129 | """第几单的退款成功时间"""
130 | return "refund_success_time_" + str(no)
131 |
132 | class ID:
133 | """ID 常量"""
134 | In = "out_refund_no" # 商户退款 ID
135 | Out = "refund_id" # 微信退款单号
136 |
137 | class Fee:
138 | """支付金额常量"""
139 | Amount = "refund_fee" # 退款金额
140 | Currency = "refund_fee_type" # 退款币种
141 |
--------------------------------------------------------------------------------
/leaf/payments/wxpay/error.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/leaf/payments/wxpay/error.json
--------------------------------------------------------------------------------
/leaf/payments/wxpay/error.py:
--------------------------------------------------------------------------------
1 | """微信支付错误类定义"""
2 |
3 | from ...core.error import Error
4 |
5 |
6 | class WXPayError(Error):
7 | """微信支付错误"""
8 | code = 19000
9 | description = "微信支付错误父类"
10 |
11 |
12 | class WXPaySignatureError(WXPayError):
13 | """微信支付签名错误"""
14 | code = 19001
15 | description = "签名验证错误 - 警惕端口扫描"
16 |
--------------------------------------------------------------------------------
/leaf/payments/wxpay/methods.py:
--------------------------------------------------------------------------------
1 | """微信支付方式"""
2 |
3 | from typing import NoReturn
4 | from . import const
5 |
6 |
7 | class WXPaymentType:
8 | """支付类型补充函数"""
9 | @staticmethod
10 | def jsapi(orderinfo: dict) -> NoReturn:
11 | """
12 | 静态函数 - 对支付信息添加支付类型信息 - jaspi
13 | *orderinfo:dict - 支付信息字典
14 | """
15 | orderinfo[const.WXPayOrder.Info.Type] = \
16 | const.WXPayOrder.Type.JSAPI
17 |
18 | @staticmethod
19 | def native(orderinfo: dict) -> NoReturn:
20 | """
21 | 静态函数 - 对支付信息添加支付类型信息 - native
22 | *orderinfo:dict - 支付信息字典
23 | """
24 | orderinfo[const.WXPayOrder.Info.Type] = \
25 | const.WXPayOrder.Type.ScanQR
26 |
27 | @staticmethod
28 | def inapp(orderinfo: dict) -> NoReturn:
29 | """
30 | 静态函数 - 对支付信息添加支付类型信息 - inapp
31 | *orderinfo:dict - 支付信息字典
32 | """
33 | orderinfo[const.WXPayOrder.Info.Type] = \
34 | const.WXPayOrder.Type.InApp
35 |
--------------------------------------------------------------------------------
/leaf/payments/wxpay/settings.py:
--------------------------------------------------------------------------------
1 | """微信支付设置量"""
2 |
3 |
4 | class Order:
5 | """订单相关设置"""
6 | DefaultCurrency = "CNY" # 默认货币为人民币
7 | MoreAttr = " ..." # 当多个商品时添加的说明
8 | PaymentDescription = "微信支付" # 支付方式的名称
9 |
10 |
11 | class NetworkAndEncrypt:
12 | """网络与加密相关设置"""
13 | NonceLength = 32 # 随机字符串不长于 32 位
14 | ExternalIPProvider = "https://api.ipify.org" # 外部IP地址提供
15 |
16 |
17 | class PaymentNotify:
18 | """支付结果通知中相关设置"""
19 |
20 | # 需要从结果中提取的业务相关键
21 | Keys = {
22 | "mch_id": "mchid",
23 | "is_subscribe": "subcribe",
24 | "bank_type": "bank",
25 | "time_end": "endtime",
26 | "transaction_id": "transaction",
27 | "attach": "attach"
28 | }
29 |
--------------------------------------------------------------------------------
/leaf/payments/wxpay/signature.py:
--------------------------------------------------------------------------------
1 | """针对微信支付平台的加密功能"""
2 |
3 | import string
4 | import urllib.parse as urlparse
5 | from typing import NoReturn
6 |
7 | from ... import core
8 | from . import const
9 | from . import settings
10 |
11 |
12 | class SignatureTool:
13 | """微信支付加密模块"""
14 |
15 | def __repr__(self) -> str:
16 | """返回 repr 信息"""
17 | return ""
18 |
19 | def __init__(self, apikey: str):
20 | """
21 | 加密模块初始化:
22 | apikey: 微信商户平台 API 密钥
23 | """
24 | self.__apikey = apikey
25 |
26 | @staticmethod
27 | def _calculate(apikey: str, **paras) -> str:
28 | """
29 | 根据微信公众平台支付加密标准对包进行签名:
30 | pay.weixin.qq.com/wiki/doc/api/external/*.php?chapter=4_3
31 | 0. 对参数字典中空的值进行处理 - 不参与运算
32 | 1. 对 paras 中传入的参数按照 ASCII 码的大小顺序进行排序
33 | 2. url化参数拼接 + apikey
34 | 3. 进行MD5签名
35 | """
36 | # 首先过滤掉空参数
37 | __calculates = dict()
38 | for key, value in paras.items():
39 | if str(value):
40 | __calculates[str(key)] = str(value)
41 |
42 | # 之后对参数进行排序并添加 apikey
43 | __sorted = sorted(__calculates.items(), key=lambda item: item[0])
44 | __sorted.append((const.WXPaySignature.Key.ApiKey, apikey))
45 |
46 | # 对参数进行 URL 化拼接并进行摘要运算
47 | clearstr = urlparse.urlencode(__sorted, safe=string.punctuation)
48 | sign = core.tools.encrypt.MD5(clearstr)
49 |
50 | return sign.upper()
51 |
52 | def do(self, unsigned: dict) -> NoReturn:
53 | """
54 | 为参数字典进行签名
55 | 这里的参数字典不需要携带签名过程中的任何信息:
56 | 1. 随机字符串 - nonce_str
57 | 2. 签名 - sign
58 | 3. 签名类型 - sign_type
59 | 这些信息由签名工具自动生成, 调用者只需要关心业务相关信息
60 | """
61 | # 生成一个随机数并添加
62 | randstr = core.tools.encrypt.random(
63 | settings.NetworkAndEncrypt.NonceLength)
64 | unsigned[const.WXPaySignature.Key.Nonce] = randstr
65 |
66 | # 添加固定值 - 这里 Version 会引起不同 API 调用错误 - 取消添加
67 | # unsigned[const.WXPaySignature.Key.Version] = \
68 | # const.WXPaySignature.Value.Version
69 |
70 | unsigned[const.WXPaySignature.Key.Type] = \
71 | const.WXPaySignature.Value.Type
72 |
73 | # 计算签名
74 | siganture = self._calculate(self.__apikey, **unsigned)
75 | unsigned[const.WXPaySignature.Key.Sign] = siganture
76 |
77 | def verify(self, **signed) -> bool:
78 | """
79 | 对传入的数据包进行数据校验:
80 | 校验成功返回 True 失败返回 False
81 | 实际等同于重新计算一次签名值检查是否相同
82 | """
83 | # 获取目标数据包的 Sign 值
84 | signature_signed = signed.pop(const.WXPaySignature.Key.Sign, None)
85 | if signature_signed is None:
86 | return False
87 |
88 | # 重新计算签名
89 | signature_unsigned = self._calculate(self.__apikey, **signed)
90 |
91 | # 判断是否相同
92 | return signature_signed == signature_unsigned
93 |
--------------------------------------------------------------------------------
/leaf/payments/yandex/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guiqiqi/leaf/79e34f4b8fba8c6fd208b5a3049103dca2064ab5/leaf/payments/yandex/__init__.py
--------------------------------------------------------------------------------
/leaf/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 插件支持模块:
3 | current - 当前的插件目录
4 | manager - 插件管理器
5 | settings - 插件相关设置
6 | """
7 |
8 | from os import path as __path
9 |
10 | # 获取当前目录生成插件管理器
11 | from .manager import Manager
12 | from . import settings
13 | current = __path.split(__path.realpath(__file__))[0]
14 |
--------------------------------------------------------------------------------
/leaf/plugins/accesstoken/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 使 leaf.weixin.accesstoken 模块获得的
3 | 微信公众平台 AccessToken 有接口对外公布
4 | """
5 |
6 | from flask import abort
7 |
8 | from ... import api
9 | from ...core import modules
10 | from ...weixin.accesstoken import Patcher
11 | from ...core.abstract.plugin import Plugin
12 |
13 | plugin = Plugin(
14 | "AccessToken 导出插件",
15 | Plugin.nothing, Plugin.nothing, Plugin.nothing,
16 | author="桂小方",
17 | description="将微信模块获取到的 AccessToken 对外公布",
18 | version="beta - 0.2.1",
19 | date="2020-05-23"
20 | )
21 |
22 | # 接口公开主机域
23 | AuthorizedHost = ["127.0.0.1"]
24 |
25 | # 注册插件主域
26 | modules.plugins.register(plugin, ["accesstoken"])
27 |
28 | # 导出接口
29 | @plugin.route("/", methods=["GET"])
30 | @api.wrapper.iplimit(AuthorizedHost)
31 | @api.wrapper.wrap("accesstoken")
32 | def accesstoken():
33 | """获取当前已经缓存的 accesstoken"""
34 | patcher: Patcher = modules.weixin.accesstoken
35 | return patcher.get()
36 |
--------------------------------------------------------------------------------
/leaf/plugins/error.py:
--------------------------------------------------------------------------------
1 | """插件运行的相关错误"""
2 |
3 | # from .. import modules
4 | # from ..core.error import Messenger
5 | from ..core.error import Error
6 |
7 |
8 | class PluginImportError(Error):
9 | """插件载入过程中出错"""
10 | code = 10101
11 | description = "插件载入时出错"
12 |
13 |
14 | class PluginNotFound(Error):
15 | """无法根据给定的 ID 寻找到插件"""
16 | code = 10102
17 | description = "没有找到对应的插件"
18 |
19 |
20 | class PluginInitError(Error):
21 | """不符合规范的插件"""
22 | code = 10103
23 | description = "插件 init 函数错误"
24 |
25 |
26 | class PluginRuntimeError(Error):
27 | """插件运行期错误"""
28 | code = 10104
29 | description = "插件运行期间出现错误"
30 |
--------------------------------------------------------------------------------
/leaf/plugins/settings.py:
--------------------------------------------------------------------------------
1 | """插件的设置文件"""
2 |
3 | skipped = {"__pycache__"} # 需要跳过加载的目录列表
4 |
--------------------------------------------------------------------------------
/leaf/rbac/__init__.py:
--------------------------------------------------------------------------------
1 | """用户用户组及认证相关 - 实现基于角色的用户权限控制"""
2 |
3 | from . import jwt
4 | from . import error
5 | from . import model
6 | from . import functions
7 |
--------------------------------------------------------------------------------
/leaf/rbac/error.py:
--------------------------------------------------------------------------------
1 | """RBAC 相关的错误定义"""
2 |
3 | from ..core import error as _error
4 |
5 |
6 | class AuthenticationByIdFailed(_error.Error):
7 | """无法找到Id验证文档"""
8 | code = 10015
9 | description = "无法找到该用户的Id验证文档"
10 |
11 |
12 | class AuthenticationFailed(_error.Error):
13 | """创建/更新身份验证其余文档时密码验证未通过"""
14 | code = 10016
15 | description = "创建/更新身份验证-密码验证失败"
16 |
17 |
18 | class AuthenticationNotFound(_error.Error):
19 | """验证文档根据给定的索引未找到"""
20 | code = 10017
21 | description = "根据给定的文档索引无法查找到身份验证文档"
22 |
23 |
24 | class UserNotFound(_error.Error):
25 | """根据给定信息找不到用户"""
26 | code = 10018
27 | description = "根据给定信息找不到用户"
28 |
29 |
30 | class UserInitialized(_error.Error):
31 | """用户已经初始化完成"""
32 | code = 10019
33 | description = "用户初始化已经完成"
34 |
35 |
36 | class AccessPointNotFound(_error.Error):
37 | """访问点无法找到"""
38 | code = 10020
39 | description = "根据给定的信息找不到访问点文档"
40 |
41 |
42 | class GroupNotFound(_error.Error):
43 | """用户组文档无法找到"""
44 | code = 10021
45 | description = "根据给定的信息找不到用户组文档"
46 |
47 |
48 | class UserIndexValueBound(_error.Error):
49 | """用户索引信息已经被绑定"""
50 | code = 10022
51 | description = "您所给定的用户索引信息已经被绑定"
52 |
53 |
54 | class UserIndexTypeBound(_error.Error):
55 | """用户索引类型已经被绑定 - 当前策略不允许多绑定"""
56 | code = 10023
57 | description = "您所给定的用户索引类型已经被绑定 - 且当前策略不允许多绑定"
58 |
59 |
60 | class AuthenticationByIdCanNotDelete(_error.Error):
61 | """不允许删除根据 Id 创建的认证文档"""
62 | code = 10024
63 | description = "不能删除根据 用户Id 创建的认证文档"
64 |
65 |
66 | class InvalidToken(_error.Error):
67 | """Token 格式错误"""
68 | code = 12112
69 | description = "JWT Token 格式错误"
70 |
71 |
72 | class InvalidHeder(_error.Error):
73 | """Token 头部格式错误"""
74 | code = 12111
75 | description = "JWT Token 头部格式错误/不支持"
76 |
77 |
78 | class SignatureError(_error.Error):
79 | """签名计算失败"""
80 | code = 12113
81 | description = "JWT Token 的签名计算错误 - 检查secret是否与算法匹配"
82 |
83 |
84 | class SignatureNotValid(_error.Error):
85 | """签名验证失败"""
86 | code = 12114
87 | description = "JWT Token 签名验证错误"
88 |
89 |
90 | class TimeExpired(_error.Error):
91 | """过期错误"""
92 | code = 12115
93 | description = "JWT Token 过期"
94 |
95 |
96 | class TokenNotFound(_error.Error):
97 | """在 HTTP Header 信息中没有发现 JWT Token 信息"""
98 | code = 12116
99 | description = "在 HTTP Header 信息中没有发现 JWT Token 信息"
100 |
101 |
102 | class AuthenticationError(_error.Error):
103 | """身份验证错误"""
104 | code = 13001
105 | description = "身份验证错误"
106 |
107 |
108 | class AuthenticationDisabled(AuthenticationError):
109 | """该身份验证方式被禁用"""
110 | code = 13002
111 | description = "该身份验证方式被禁用"
112 |
113 |
114 | class InvalidExceptionAccessPonint(_error.Error):
115 | """前端传入的例外用户组无法识别/用户ID错误"""
116 | code = 13003
117 | description = "传入的例外用户组无法被识别/用户ID错误"
118 |
119 |
120 | class AccessPointNameConflicting(_error.Error):
121 | """访问点名称冲突"""
122 | code = 13004
123 | description = "访问点名称冲突"
124 |
125 |
126 | class UndefinedUserIndex(_error.Error):
127 | """未定义的用户索引类型"""
128 | code = 13005
129 | description = "未定义的用户索引类型"
130 |
--------------------------------------------------------------------------------
/leaf/rbac/functions/__init__.py:
--------------------------------------------------------------------------------
1 | """CRUD 使用到的函数集合"""
2 |
3 | from . import auth
4 | from . import user
5 | from . import group
6 | from . import accesspoint
7 |
--------------------------------------------------------------------------------
/leaf/rbac/functions/accesspoint.py:
--------------------------------------------------------------------------------
1 | """访问点所需要的函数集合"""
2 |
3 | from typing import List
4 |
5 | from .. import error
6 | from ..model import AccessPoint
7 |
8 |
9 | class Create:
10 | """创建静态函数集合"""
11 |
12 |
13 | class Retrieve:
14 | """查找静态函数集合"""
15 |
16 | @staticmethod
17 | def byname(name: str) -> AccessPoint:
18 | """根据名称查找访问点"""
19 | # pylint: disable=no-member
20 | found: List[AccessPoint] = AccessPoint.objects(pointname=name)
21 | if not found:
22 | raise error.AccessPointNotFound(name)
23 | return found[0]
24 |
25 | class Update:
26 | """更新静态函数集合"""
27 |
28 |
29 | class Delete:
30 | """删除静态函数集合"""
31 |
--------------------------------------------------------------------------------
/leaf/rbac/functions/group.py:
--------------------------------------------------------------------------------
1 | """用户组操作的相关函数集合"""
2 |
3 | from typing import List
4 |
5 | from bson import ObjectId
6 |
7 | from .. import error
8 | from ..model.group import Group
9 |
10 |
11 | class Create:
12 | """创建用户组的静态函数集合"""
13 |
14 |
15 | class Retrieve:
16 | """查询用户组的静态函数集合"""
17 |
18 | @staticmethod
19 | def byid(groupid: ObjectId) -> Group:
20 | """通过用户组 ID 查找用户组记录"""
21 | # pylint: disable=no-member
22 | found: List[Group] = Group.objects(id=groupid)
23 | if not found:
24 | raise error.GroupNotFound(str(groupid))
25 | return found[0]
26 |
27 |
28 | class Update:
29 | """更新/编辑用户组的静态函数集合"""
30 |
31 |
32 | class Delete:
33 | """删除用户组的静态函数结合"""
34 |
--------------------------------------------------------------------------------
/leaf/rbac/functions/user.py:
--------------------------------------------------------------------------------
1 | """用户相关的数据库操作函数集合"""
2 |
3 | from typing import List, NoReturn
4 |
5 | import mongoengine
6 | from bson import ObjectId
7 |
8 | from . import auth
9 | from . import group
10 | from .. import error
11 | from .. import settings
12 | from ..model import User
13 | from ..model import UserIndex
14 |
15 |
16 | class Create:
17 | """创建用户静态函数集合"""
18 |
19 | @staticmethod
20 | def index(userid: ObjectId, userindex: UserIndex) -> List[UserIndex]:
21 | """
22 | 该函数将为用户新增一种索引类型:
23 | 1. 首先会检查指定的索引值是否被其他用户绑定过
24 | 2. 尝试插入检查是否发生 unique 错误
25 | """
26 | # 检查是否其他用户已经绑定过
27 | typeid, value = userindex.typeid, userindex.value
28 | if Retrieve.byindex(typeid, value):
29 | raise error.UserIndexValueBound(typeid + " - " + value)
30 |
31 | try:
32 | user: User = Retrieve.byid(userid)
33 | user.indexs.append(userindex)
34 | except mongoengine.NotUniqueError as _error:
35 | raise error.UserIndexTypeBound(typeid)
36 | else:
37 | user.indexs.save()
38 | return user.indexs
39 |
40 | @staticmethod
41 | def group(userid: ObjectId, groupid: ObjectId) -> User:
42 | """
43 | 向组中添加用户:
44 | 向用户组的用户记录中添加
45 | 向用户的组记录中添加
46 | """
47 | userid = ObjectId(userid)
48 | groupid = ObjectId(groupid)
49 | user = Retrieve.byid(userid)
50 | ugroup = group.Retrieve.byid(groupid)
51 | if not ugroup in user.groups:
52 | user.groups.append(ugroup)
53 |
54 | if not userid in ugroup.users:
55 | ugroup.users.append(userid)
56 | ugroup.save()
57 |
58 | return user.save()
59 |
60 |
61 | class Retrieve:
62 | """查询用户静态函数集合"""
63 |
64 | @staticmethod
65 | def byindex(typeid: str, value: str) -> List[User]:
66 | """通过 Index 用户索引文档查询用户记录"""
67 | # pylint: disable=no-member
68 | return User.objects(indexs__value=value,
69 | indexs__typeid=typeid)
70 |
71 | @staticmethod
72 | def byid(userid: ObjectId) -> User:
73 | """通过用户 Id 查询用户文档"""
74 | # pylint: disable=no-member
75 | users = User.objects(id=userid)
76 | if not users:
77 | raise error.UserNotFound(str(userid))
78 | return users[0]
79 |
80 | @staticmethod
81 | def initialized(userid: ObjectId) -> bool:
82 | """查询用户是否被初始化"""
83 | user: User = Retrieve.byid(userid)
84 | if not user.indexs:
85 | return False
86 | return True
87 |
88 |
89 | class Update:
90 | """更新用户静态函数集合"""
91 |
92 | @staticmethod
93 | def inituser(userid: ObjectId) -> User:
94 | """
95 | 该函数将为用户创建ID索引
96 | 创建一个用户的接口调用顺序如下:
97 | 1. 首先调用创建用户接口 - View 层处理
98 | 2. 调用为用户设置密码接口 - auth.Create.withuserid
99 | 3. 为用户设置文档ID索引 - 该接口
100 | """
101 | try:
102 | user: User = Retrieve.byid(userid)
103 |
104 | # 为用户创建Id索引
105 | typeid, description = settings.User.Indexs.Id
106 | idindex = UserIndex(
107 | typeid=typeid, value=str(userid),
108 | description=description)
109 | user.indexs.append(idindex)
110 |
111 | except mongoengine.NotUniqueError as _error:
112 | raise error.UserInitialized(str(userid))
113 | except IndexError as _error:
114 | raise error.UserNotFound(str(userid))
115 | else:
116 | return user.save()
117 |
118 |
119 | class Delete:
120 | """删除用户静态函数集合"""
121 |
122 | @staticmethod
123 | def index(userid: ObjectId, typeid: str) -> NoReturn:
124 | """
125 | 删除用户的某一种 Index
126 | 同时要删除该 Index 对应的 Auth 文档
127 | """
128 |
129 | user: User = Retrieve.byid(userid)
130 | try:
131 | index = user.indexs.get(typeid=typeid)
132 | except mongoengine.errors.DoesNotExist as _error:
133 | return user.indexs
134 |
135 | user.indexs.remove(index)
136 | user.indexs.save()
137 |
138 | # 查找并删除指定的 Auth 文档
139 | # pylint: disable=no-member
140 | try:
141 | authdoc = auth.Retrieve.byindex(index.value)
142 | except error.AuthenticationNotFound as _error:
143 | pass
144 | else:
145 | authdoc.delete()
146 |
147 | return user.indexs
148 |
149 | @staticmethod
150 | def group(userid: ObjectId, groupid: ObjectId) -> NoReturn:
151 | """
152 | 将用户从某个用户组中移除:
153 | 将用户从用户组中移除
154 | 将组中的用户记录删除
155 | """
156 | userid = ObjectId(userid)
157 | groupid = ObjectId(groupid)
158 | user = Retrieve.byid(userid)
159 | ugroup = group.Retrieve.byid(groupid)
160 | if ugroup in user.groups:
161 | user.groups.remove(ugroup)
162 | user.save()
163 |
164 | if userid in ugroup.users:
165 | ugroup.users.remove(userid)
166 | ugroup.save()
167 |
--------------------------------------------------------------------------------
/leaf/rbac/jwt/__init__.py:
--------------------------------------------------------------------------------
1 | """JWT Token 包"""
2 |
3 | from . import const
4 | from . import settings
5 |
6 | from .token import Token
7 | from .verify import Verify
8 |
--------------------------------------------------------------------------------
/leaf/rbac/jwt/const.py:
--------------------------------------------------------------------------------
1 | """JWT Token常量相关设置"""
2 |
3 |
4 | class Header:
5 | """头部相关设置"""
6 | Type = "typ" # 类型
7 | Algorithm = "alg" # 加密算法
8 |
9 | @staticmethod
10 | def make(algorithm: str) -> dict:
11 | """生成头部"""
12 | return {
13 | Header.Type: "JWT",
14 | Header.Algorithm: algorithm
15 | }
16 |
17 |
18 | class Payload:
19 | """载荷相关常量"""
20 | Issuer = "iss" # 签发者
21 | Expiration = "exp" # 过期时间
22 | IssuedAt = "iat" # 签发时间
23 | Audience = "aud" # 颁发对象
24 |
--------------------------------------------------------------------------------
/leaf/rbac/jwt/settings.py:
--------------------------------------------------------------------------------
1 | """JWT Token相关设置"""
2 |
3 | from ...core.tools import encrypt
4 |
5 |
6 | class Signature:
7 | """签名相关设置"""
8 | Algorithm = ("HS256", encrypt.HMAC_SHA256) # 默认签名算法
9 | ValidPeriod = 3600 # 默认 Token 有效期
10 |
11 |
12 | class Payload:
13 | """自定义载荷部分设置"""
14 | Permission = "permission" # 签发时的用户权限
15 |
--------------------------------------------------------------------------------
/leaf/rbac/jwt/token.py:
--------------------------------------------------------------------------------
1 | """JWT Token 类"""
2 |
3 | from typing import Tuple, Optional, Callable, NoReturn, Dict
4 |
5 | from ...core.tools import web
6 | from ...core.tools import time
7 | from ...core.tools import encrypt
8 |
9 | from . import const
10 | from . import settings
11 |
12 |
13 | class Token:
14 | """Token 计算与生成器"""
15 |
16 | def __init__(self, secret: str):
17 | """
18 | Token 类构造函数:
19 | parts: 部分令牌
20 | secret: 签名密钥
21 | algorithm: 在设置头部时指定算法
22 | """
23 | self.__parts = list()
24 | self.__secret: str = secret
25 | self.__algorithm: Callable[[bytes, bytes], str] = None
26 |
27 | def header(self, algorithm: Optional[Tuple[str, Callable]]
28 | = settings.Signature.Algorithm) -> NoReturn:
29 | """计算头部字符串"""
30 | header = const.Header.make(algorithm[0])
31 | self.__algorithm = algorithm[1]
32 | text = web.JSONcreater(header)
33 | self.__parts.append(encrypt.base64encode_url(text.encode()).decode())
34 |
35 | def payload(self, issuer: str, audience: str,
36 | period: int = settings.Signature.ValidPeriod,
37 | other: Optional[Dict[str, str]] = None) -> NoReturn:
38 | """计算载荷部分的值"""
39 | now = time.now()
40 | payload = {
41 | const.Payload.Issuer: issuer,
42 | const.Payload.Audience: audience,
43 | const.Payload.IssuedAt: now,
44 | const.Payload.Expiration: now + period
45 | }
46 | if not other is None:
47 | payload.update(other)
48 | text = web.JSONcreater(payload)
49 | self.__parts.append(encrypt.base64encode_url(text.encode()).decode())
50 |
51 | def issue(self) -> str:
52 | """签名之后返回可用的Token"""
53 | content = '.'.join(self.__parts)
54 | signature = self.__algorithm(content.encode(), self.__secret.encode())
55 | return content + '.' + encrypt.base64encode_url(signature).decode()
56 |
--------------------------------------------------------------------------------
/leaf/rbac/jwt/verify.py:
--------------------------------------------------------------------------------
1 | """JWT Token 验证模块"""
2 |
3 | from typing import NoReturn, Callable, Optional, Tuple
4 |
5 | from . import const
6 | from . import settings
7 | from .. import error
8 | from ...core.tools import web
9 | from ...core.tools import time
10 | from ...core.tools import encrypt
11 |
12 |
13 | class Verify:
14 | """JWT Token 验证类"""
15 |
16 | def __init__(self, token: str):
17 | """
18 | JWT Token验证类:
19 | 分割并检查Token格式是否正确
20 | 还原被encode的信息
21 | algorithm - 使用的加密算法
22 | content - 无签名部分的token
23 | """
24 | try:
25 | header, payload, signature = token.split('.')
26 | except ValueError as _error:
27 | raise error.InvalidToken(token)
28 |
29 | self.__algorithm: Callable[[bytes, bytes], str] = None
30 | self.__header: str = encrypt.base64decode_url(header.encode()).decode()
31 | self.__payload: str = encrypt.base64decode_url(payload.encode()).decode()
32 | self.__content: str = header + '.' + payload
33 | self.__signature: bytes = encrypt.base64decode_url(signature.encode())
34 |
35 | def header(self, algorithm: Optional[Tuple[str, Callable]]
36 | = settings.Signature.Algorithm) -> dict:
37 | """验证头部信息是否正确"""
38 | header = web.JSONparser(self.__header)
39 | target = const.Header.make(algorithm[0])
40 | self.__algorithm = algorithm[1]
41 |
42 | if target != header:
43 | raise error.InvalidHeder(str(header))
44 |
45 | return header
46 |
47 | def payload(self) -> dict:
48 | """验证是否过期"""
49 | payload = web.JSONparser(self.__payload)
50 | now = time.now()
51 | expired = payload.get(const.Payload.Expiration)
52 | if now > expired:
53 | raise error.TimeExpired("过期在: " + time.timestr(expired))
54 |
55 | return payload
56 |
57 | def signature(self, secret: str) -> NoReturn:
58 | """验证签名是否正确"""
59 | target = self.__algorithm(self.__content.encode(), secret.encode())
60 | if target != self.__signature:
61 | raise error.SignatureNotValid(self.__signature)
62 |
--------------------------------------------------------------------------------
/leaf/rbac/model/__init__.py:
--------------------------------------------------------------------------------
1 | """用户, 组, 及相关认证数据库模型"""
2 |
3 | from .group import Group
4 | from .user import User
5 | from .user import UserIndex
6 | from .auth import Authentication
7 | from .accesspoint import AccessPoint
8 |
--------------------------------------------------------------------------------
/leaf/rbac/model/accesspoint.py:
--------------------------------------------------------------------------------
1 | """访问权限点的数据库模型建立"""
2 |
3 | import mongoengine
4 |
5 | from .user import User
6 |
7 |
8 | class AccessPoint(mongoengine.Document):
9 | """
10 | 权限访问点的数据库模型:
11 | pointname: 权限点名称(e.g. leaf.plugins.wxtoken.get)
12 | required: 需求的最小权限值
13 | strict: 是否要求仅仅指定权限值的用户可访问
14 | description: 当前权限点的描述
15 | exceptions: 针对某些用户的特例
16 | """
17 |
18 | pointname = mongoengine.StringField(primary_key=True)
19 | required = mongoengine.IntField(required=True)
20 | strict = mongoengine.BooleanField(default=False)
21 | description = mongoengine.StringField(default=str)
22 | exceptions = mongoengine.ListField(
23 | field=mongoengine.LazyReferenceField(User), default=list)
24 |
--------------------------------------------------------------------------------
/leaf/rbac/model/auth.py:
--------------------------------------------------------------------------------
1 | """认证记录数据库模型"""
2 |
3 | import mongoengine
4 |
5 | from .user import User
6 |
7 |
8 | class Authentication(mongoengine.Document):
9 | """
10 | 身份验证文档模型:
11 | user: 被建立登陆方式的用户id
12 | index: 用户索引(账户名-主键)
13 | salt: 用户密码盐
14 | token: 用户密码加盐之后的哈希值
15 | status: 验证方式是否可用
16 | description: 身份验证方式描述
17 | """
18 |
19 | index = mongoengine.StringField(primary_key=True)
20 | user = mongoengine.LazyReferenceField(
21 | User, reverse_delete_rule=mongoengine.CASCADE)
22 | salt = mongoengine.StringField(required=True)
23 | token = mongoengine.StringField(required=True)
24 | status = mongoengine.BooleanField(default=True)
25 | description = mongoengine.StringField(default=str)
26 |
--------------------------------------------------------------------------------
/leaf/rbac/model/group.py:
--------------------------------------------------------------------------------
1 | """用户组数据库模型"""
2 |
3 | import mongoengine
4 |
5 |
6 | class Group(mongoengine.Document):
7 | """
8 | 用户组模型:
9 | name: 用户组的名称
10 | description: 用户组的描述信息
11 | permission: 用户组的权限值
12 | users: 用户组中包含的用户
13 | extensions: 扩展信息存储
14 | """
15 |
16 | name = mongoengine.StringField(required=True, default=str)
17 | description = mongoengine.StringField(default=str)
18 | permission = mongoengine.IntField(required=True, default=int)
19 | users = mongoengine.ListField(mongoengine.ObjectIdField())
20 | extensions = mongoengine.MapField(field=mongoengine.StringField())
21 |
--------------------------------------------------------------------------------
/leaf/rbac/model/user.py:
--------------------------------------------------------------------------------
1 | """用户数据模型"""
2 |
3 | import mongoengine
4 |
5 | from .. import settings
6 | from ...core.tools import time
7 | from .group import Group
8 |
9 |
10 | class UserIndex(mongoengine.EmbeddedDocument):
11 | """
12 | 用户索引内嵌文档模型:
13 | typeid: 类型id 同一种索引方式相同
14 | value: 用户索引值
15 | description: 用户索引描述
16 | extension: 扩展描述字典
17 | """
18 |
19 | typeid = mongoengine.StringField(required=True)
20 | value = mongoengine.StringField(required=True)
21 | description = mongoengine.StringField(default=str)
22 | extension = mongoengine.DictField()
23 |
24 |
25 | class User(mongoengine.Document):
26 | """
27 | 用户数据库模型:
28 | created: 创建时间 默认为utc时间
29 | disabled: 用户是否已被禁用
30 | groups: 用户被分配到的用户组信息
31 | indexs: 用户的索引信息列表
32 | informations: 用户的个人信息
33 | """
34 |
35 | created = mongoengine.IntField(default=time.now)
36 | disabled = mongoengine.BooleanField(default=False)
37 | groups = mongoengine.ListField(mongoengine.ReferenceField(
38 | Group, reverse_delete_rule=mongoengine.PULL), default=list)
39 | indexs = mongoengine.EmbeddedDocumentListField(UserIndex, default=list)
40 | avatar = mongoengine.ImageField(
41 | size=settings.User.AvatarSize,
42 | thumbnail_size=settings.User.AvatarThumbnailSize)
43 | informations = mongoengine.DictField()
44 |
--------------------------------------------------------------------------------
/leaf/rbac/settings.py:
--------------------------------------------------------------------------------
1 | """用户, 组, 认证相关的设置文件"""
2 |
3 | from collections import namedtuple
4 | from ..core.algorithm import StaticDict
5 |
6 |
7 | Index = namedtuple("Index", ("typeid", "description"))
8 |
9 |
10 | class User:
11 | """用户相关的设置文件"""
12 | # 是否允许同一种索引方式存在多个
13 | # 例如同账户绑定了多个不同的微信号
14 | # 默认情况不被允许
15 | # 选项被禁用 - 2020.2.19
16 | # AllowMultiAccountBinding = False
17 |
18 | # 头像与缩略图大小 - (width, height, 是否强制缩放)
19 | AvatarSize = (80, 80, True)
20 | AvatarThumbnailSize = (50, 50, True)
21 | AvatarType = {"jpg", "jpeg", "png", "gif"}
22 |
23 | Indexs = StaticDict({
24 | # 根据 Id 的索引方式请勿删除 - 会导致错误
25 | "Id": Index("1B4E705F3305F7FB", "通过用户ID索引"), # 通过用户id索引
26 |
27 | # 下面的索引方式可以拓展
28 | "Mail": Index("EAC366AD5FEA1B28", "通过邮件索引"), # 通过邮件索引
29 | "Name": Index("0C6B4A2B8AAEDDBC", "通过用户名索引"), # 通过用户名索引
30 | "Phone": Index("5E4BC1ABDDAACA4A", "通过手机号索引") # 通过手机号索引
31 | })
32 |
33 |
34 | class Authentication:
35 | """认证相关的设置文件"""
36 |
37 | class Security:
38 | """安全策略设置"""
39 | SaltLength = 128 # 密钥盐的长度(bits)
40 | SaltCahce = 128 # 盐数据库查询的缓存数量
41 | PasswordHashCycle = 4 # 对密码进行迭代哈希的次数
42 |
43 | class Description:
44 | """描述字符串"""
45 | Id = "通过用户ID验证"
46 | Mail = "通过邮相验证"
47 | Name = "通过用户名验证"
48 | Phone = "通过电话验证"
49 |
--------------------------------------------------------------------------------
/leaf/selling/__init__.py:
--------------------------------------------------------------------------------
1 | """SKU/SPU/销售相关"""
2 |
3 | from . import order
4 | from . import error
5 | from . import commodity
6 | from . import settings
7 |
--------------------------------------------------------------------------------
/leaf/selling/commodity/__init__.py:
--------------------------------------------------------------------------------
1 | """SPU与产品管理相关"""
2 |
3 | from .stock import Stock
4 | from .product import Product
5 | from .product import ProductParameter
6 | from .generator import StocksGenerator
7 |
--------------------------------------------------------------------------------
/leaf/selling/commodity/generator.py:
--------------------------------------------------------------------------------
1 | """使用树算法根据产品生成商品信息"""
2 |
3 | from queue import Queue
4 |
5 | from typing import List
6 | from typing import NoReturn
7 |
8 | from .stock import Stock
9 | from .product import Product
10 | from .product import ProductParameter
11 |
12 | from ...core.algorithm import tree
13 |
14 |
15 | class StocksGenerator:
16 | """
17 | 使用树算法根据传入的产品信息生成商品并设置
18 | """
19 |
20 | def __init__(self, product: Product) -> NoReturn:
21 | """
22 | 类初始化函数:
23 | product: 要使用进行生成的产品信息
24 | """
25 | self.__product = product
26 | self.__stocks: List[Stock] = list()
27 |
28 | @property
29 | def goods(self) -> List[Stock]:
30 | """返回内存中的所有商品列表"""
31 | return self.__stocks
32 |
33 | def setall(self, price: float, currency: str, inventory: int) -> NoReturn:
34 | """
35 | 对所有的商品设置统一信息:
36 | price: 商品价格
37 | currency: 商品价格货币
38 | inventory: 商品库存
39 | """
40 | for good in self.__stocks:
41 | good.price = price
42 | good.currency = currency
43 | good.inventory = inventory
44 |
45 | @staticmethod
46 | def _generate(product: Product) -> tree.Tree:
47 | """
48 | 使用 BFS 算法生成一棵产品信息树:
49 | 0. 生成 None 的根节点, 将根节点加入任务队列
50 | 1. 将当前遍历属性的 index 加入任务节点
51 | 2. 获取任务节点以及需要添加的信息
52 | 3. goto 0 until tasks.qsize() == 0
53 | """
54 | index, root, height = 0, tree.Node(None), len(product.parameters)
55 | tasks = Queue()
56 | tasks.put((root, index))
57 |
58 | # 开始 BFS 的主循环
59 | while tasks.qsize():
60 | node, index = tasks.get()
61 | option: ProductParameter = product.parameters[index]
62 | children = [tree.Node(option.name, selection)
63 | for selection in option.options]
64 | node.adds(children)
65 | if index == height - 1:
66 | continue
67 | for task in children:
68 | tasks.put((task, index + 1))
69 |
70 | return tree.Tree(root)
71 |
72 | @staticmethod
73 | def _traverse(infotree: tree.Tree) -> List[dict]:
74 | """遍历所有的叶子节点路径, 将节点信息取出之后update"""
75 | goods = list()
76 |
77 | for leaf, path in infotree.leaves():
78 | attributes = dict()
79 | # 加入叶子结点的信息
80 | attributes.update({leaf.tag: leaf.value})
81 |
82 | for node in path:
83 | attributes.update({node.tag: node.value})
84 |
85 | # 将根节点的无意义数据 pop 出去
86 | attributes.pop(None)
87 | goods.append(attributes)
88 |
89 | return goods
90 |
91 | def clean(self) -> int:
92 | """
93 | 对现有的产品信息中的旧商品信息进行清理
94 | 这个操作会清除数据库中所有产品生成的商品记录
95 | """
96 | # pylint: disable=no-member
97 | return Stock.objects(product=self.__product).delete()
98 |
99 | def save(self) -> int:
100 | """将内存中的所有商品对象入库"""
101 | rows = len(self.__stocks)
102 | for good in self.__stocks:
103 | good.save()
104 |
105 | self.__stocks = []
106 | return rows
107 |
108 | def calculate(self) -> NoReturn:
109 | """生成与遍历树 - 内存中生成所有的商品对象"""
110 | ctree = self._generate(self.__product)
111 | attributes = self._traverse(ctree)
112 |
113 | for attribute in attributes:
114 | good = Stock(individual=False,
115 | product=self.__product,
116 | name=self.__product.name,
117 | attributes=attribute,
118 | description=self.__product.description,
119 | addition=self.__product.addition,
120 | tags=self.__product.tags)
121 | self.__stocks.append(good)
122 |
--------------------------------------------------------------------------------
/leaf/selling/commodity/product.py:
--------------------------------------------------------------------------------
1 | """产品类型数据库模型"""
2 |
3 | import mongoengine
4 |
5 |
6 | class ProductParameter(mongoengine.EmbeddedDocument):
7 | """
8 | 产品参数内嵌文档:
9 | name: 选项名称
10 | options: 可选项值列表
11 | """
12 |
13 | name = mongoengine.StringField()
14 | options = mongoengine.ListField()
15 |
16 |
17 | class Product(mongoengine.Document):
18 | """
19 | 产品类数据模型:
20 | name: 产品名称
21 | description: 产品描述
22 | addtion: 产品额外描述
23 | tags: 产品标签列表
24 | onsale: 产品是否上架
25 | parameters: 产品可选项目列表
26 | extension: 扩展数据存储
27 | """
28 |
29 | name = mongoengine.StringField()
30 | description = mongoengine.StringField()
31 | addition = mongoengine.StringField()
32 | tags = mongoengine.ListField(field=mongoengine.StringField())
33 | parameters = mongoengine.EmbeddedDocumentListField(ProductParameter)
34 | onsale = mongoengine.BooleanField(default=True)
35 | extensions = mongoengine.DictField(default=dict)
36 |
--------------------------------------------------------------------------------
/leaf/selling/commodity/stock.py:
--------------------------------------------------------------------------------
1 | """商品数据模型"""
2 |
3 | import mongoengine
4 |
5 | from .product import Product
6 | from .. import settings
7 |
8 |
9 | class Stock(mongoengine.Document):
10 | """
11 | 商品数据模型:
12 | individual: 是否是独立的商品(非产品子类)
13 | attributes: 由产品生成时记录该商品的选项信息
14 | product: 父级产品类(当individual为False时)
15 | name: 商品名称(当individual为True时)
16 | description: 商品描述(当individual为True时)
17 | addition: 商品附加信息(当individual为True时)
18 | tags: 商品标签列表(当individual为True时)
19 | price: 商品价格
20 | currency: 商品货币单位 ISO-4217
21 | inventory: 商品库存数量
22 | onsale: 商品是否上架
23 | extensions: 扩展数据存储
24 | """
25 |
26 | individual = mongoengine.BooleanField(required=True, default=False)
27 | product = mongoengine.ReferenceField(
28 | Product, reverse_delete_rule=mongoengine.CASCADE)
29 | name = mongoengine.StringField()
30 | attributes = mongoengine.DictField()
31 | description = mongoengine.StringField()
32 | addition = mongoengine.StringField()
33 | tags = mongoengine.ListField(mongoengine.StringField())
34 | price = mongoengine.FloatField()
35 | currency = mongoengine.StringField(default=settings.General.DefaultCurrency)
36 | inventory = mongoengine.IntField()
37 | onsale = mongoengine.BooleanField(default=True)
38 | extensions = mongoengine.DictField(default=dict)
39 |
--------------------------------------------------------------------------------
/leaf/selling/error.py:
--------------------------------------------------------------------------------
1 | """销售相关错误定义"""
2 |
3 | from ..core import error as _error
4 |
5 |
6 | class ProductNotFound(_error.Error):
7 | """根据给定信息找不到对应的产品"""
8 | code = 10025
9 | description = "根据给定信息找不到对应的产品"
10 |
11 |
12 | class ProductParameterNotFound(_error.Error):
13 | """找不到对应的产品参数信息"""
14 | code = 10026
15 | description = "找不到对应的产品参数信息"
16 |
17 |
18 | class ProductParameterConflicting(_error.Error):
19 | """产品参数信息冲突"""
20 | code = 10027
21 | description = "产品参数信息发现重复"
22 |
23 |
24 | class StockNotFound(_error.Error):
25 | """根据给定信息找不到对应商品"""
26 | code = 10028
27 | description = "根据给定信息找不到对应的商品"
28 |
29 |
30 | class InvalidCurrency(_error.Error):
31 | """非允许的交易货币类型"""
32 | code = 10029
33 | description = "不允许给定的货币类型进行交易"
34 |
35 |
36 | class EmptyOrder(_error.Error):
37 | """试图创建一个空订单信息"""
38 | code = 10030
39 | description = "不能用空商品列表创建订单"
40 |
41 |
42 | class DiffrentCurrencies(_error.Error):
43 | """创建订单的商品货币类型不统一"""
44 | code = 10031
45 | description = "商品货币类型不统一"
46 |
47 |
48 | class DiscontinueStock(_error.Error):
49 | """给定的商品已经停售"""
50 | code = 10032
51 | description = "试图创建订单的商品已经停售"
52 |
53 |
54 | class InsufficientInventory(_error.Error):
55 | """库存不足"""
56 | code = 10033
57 | description = "所选商品库存不足"
58 |
59 |
60 | class InvalidPaymentCallback(_error.Error):
61 | """支付平台通知到的订单信息找不到了 - 这可是很严重的错误"""
62 | code = 10034
63 | description = "无法找到支付平台通知到的订单"
64 |
--------------------------------------------------------------------------------
/leaf/selling/functions/__init__.py:
--------------------------------------------------------------------------------
1 | """产品销售相关的 RBAC 业务层函数"""
2 |
3 | from . import product
4 |
--------------------------------------------------------------------------------
/leaf/selling/functions/product.py:
--------------------------------------------------------------------------------
1 | """产品相关的 RBAC 函数"""
2 |
3 | from typing import List, Dict
4 | from collections import defaultdict
5 |
6 | import cacheout
7 | import mongoengine
8 | from bson import ObjectId
9 |
10 | from .. import error
11 | from .. import settings
12 | from ..commodity import Product
13 | from ..commodity import ProductParameter
14 |
15 |
16 | class Create:
17 | """Product 创建类"""
18 |
19 | @staticmethod
20 | def parameter(productid: ObjectId, name: str,
21 | options: List[str]) -> List[ProductParameter]:
22 | """给产品增加一个参数选项"""
23 | product = Retrieve.byid(productid)
24 | parameter = ProductParameter(name=name, options=options)
25 |
26 | try:
27 | product.parameters.get(name=name)
28 | except mongoengine.errors.DoesNotExist as _error:
29 | product.parameters.append(parameter)
30 | else:
31 | raise error.ProductParameterConflicting(name)
32 |
33 | product.parameters.save()
34 | return product.parameters
35 |
36 |
37 | class Update:
38 | """产品信息更新类"""
39 |
40 | @staticmethod
41 | def onsale(productid: ObjectId, status: bool) -> Product:
42 | """更新产品在售信息"""
43 | product = Retrieve.byid(productid)
44 | product.onsale = status
45 | return product.save()
46 |
47 |
48 | class Retrieve:
49 | """Product 信息查询类"""
50 |
51 | tags_cache = cacheout.Cache(ttl=settings.Product.TagsCacheTime)
52 |
53 | @staticmethod
54 | def byid(productid: ObjectId) -> Product:
55 | """根据产品 Id 查询产品"""
56 | # pylint: disable=no-member
57 | product: List[Product] = Product.objects(id=productid)
58 | if not product:
59 | raise error.ProductNotFound(productid)
60 | return product[0]
61 |
62 | @staticmethod
63 | def byname(name: str) -> List[Product]:
64 | """根据名称查找产品"""
65 | # pylint: disable=no-member
66 | return Product.objects(name__icontains=name)
67 |
68 | @staticmethod
69 | def bytags(tags: List[str]) -> List[Product]:
70 | """根据产品标签查找产品"""
71 | # 首先对标签进行头尾空格处理, 小写化处理
72 | tags = [tag.strip().lower() for tag in tags]
73 |
74 | #pylint: disable=no-member
75 | queryset: List[Product] = Product.objects(tags=tags[0])
76 | for tag in tags[1::]:
77 | queryset = queryset.filter(tags=tag)
78 | return queryset
79 |
80 | @staticmethod
81 | @tags_cache.memoize()
82 | def tags() -> Dict[str, int]:
83 | """查询全部的 Tags"""
84 | tags = defaultdict(int)
85 | # pylint: disable=no-member
86 | for product in Product.objects:
87 | for tag in product.tags:
88 | tags[tag] += 1
89 |
90 | return tags
91 |
92 |
93 | class Delete:
94 | """删除相关操作函数集合"""
95 |
96 | @staticmethod
97 | def parameter(productid: ObjectId, name: str) -> List[ProductParameter]:
98 | """删除产品的的一个指定参数"""
99 | product = Retrieve.byid(productid)
100 | try:
101 | parameter: ProductParameter = product.parameters.get(name=name)
102 | except mongoengine.errors.DoesNotExist as _error:
103 | raise error.ProductParameterNotFound(name)
104 |
105 | product.parameters.remove(parameter)
106 | product.parameters.save()
107 | return product.parameters
108 |
--------------------------------------------------------------------------------
/leaf/selling/functions/stock.py:
--------------------------------------------------------------------------------
1 | """商品的相关CRUD函数"""
2 |
3 | from typing import List
4 | from bson import ObjectId
5 |
6 | from .. import error
7 | from ..commodity import Stock
8 |
9 | class Retrieve:
10 | """查询相关函数"""
11 |
12 | @staticmethod
13 | def byid(goodid: ObjectId) -> Stock:
14 | """根据商品ID查询商品"""
15 | # pylint: disable=no-member
16 | goods: List[Stock] = Stock.objects(id=goodid)
17 | if not goods:
18 | raise error.StockNotFound(goodid)
19 | return goods[0]
20 |
21 | @staticmethod
22 | def byname(name: str, individual: bool = False) -> List[Stock]:
23 | """根据名称查找商品"""
24 | # pylint: disable=no-member
25 | if individual:
26 | return Stock.objects(name__icontains=name, individual=True)
27 | return Stock.objects(name__icontains=name)
28 |
29 | @staticmethod
30 | def bytags(tags: List[str], individual: bool = False) -> List[Stock]:
31 | """根据标签查找商品"""
32 | # 首先对标签进行头尾空格处理, 小写化处理
33 | tags = [tag.strip().lower() for tag in tags]
34 |
35 | #pylint: disable=no-member
36 | if individual:
37 | queryset: List[Stock] = Stock.objects(tags=tags[0], individual=True)
38 | else:
39 | queryset: List[Stock] = Stock.objects(tags=tags[0])
40 |
41 | for tag in tags[1::]:
42 | queryset = queryset.filter(tags=tag)
43 | return queryset
44 |
45 |
46 | class Delete:
47 | """删除相关函数"""
48 |
49 |
50 | class Create:
51 | """创建相关函数"""
52 |
53 |
54 | class Update:
55 | """更新相关函数"""
56 |
57 | @staticmethod
58 | def onsale(goodid: str, status: bool) -> Stock:
59 | """更新商品在售信息"""
60 | good = Retrieve.byid(goodid)
61 | good.onsale = status
62 | return good.save()
63 |
--------------------------------------------------------------------------------
/leaf/selling/order/__init__.py:
--------------------------------------------------------------------------------
1 | """订单相关的模型与文件"""
2 |
3 | from . import events
4 | from . import status
5 | from . import manager
6 | from . import settings
7 | from .order import Order
8 |
--------------------------------------------------------------------------------
/leaf/selling/order/events.py:
--------------------------------------------------------------------------------
1 | """所有的订单状态事件"""
2 | # pylint: disable=arguments-differ
3 |
4 | from typing import NoReturn, Dict
5 |
6 | from ...core.tools import encrypt
7 | from ...core.abstract import payment
8 | from ...core.algorithm import fsm
9 |
10 | from .settings import Events as settings
11 |
12 |
13 | class Confirm(fsm.Event):
14 | """用户确认订单"""
15 | description: str = settings.Description.Confirm
16 |
17 |
18 | class UserClose(fsm.Event):
19 | """用户主动关闭订单"""
20 | description: str = settings.Description.UserClose
21 |
22 | def action(self, reason: str) -> NoReturn:
23 | """记录用户主动关闭订单的原因"""
24 | self.append(settings.ExtraInformation.CloseReason, reason)
25 |
26 |
27 | class Paying(fsm.Event):
28 | """用户正在付款"""
29 | description: str = settings.Description.Paying
30 |
31 | def action(self, payments: Dict[payment.AbstractPayment, float]) -> NoReturn:
32 | """用户支付的支付方式以及金额进行保存"""
33 |
34 | items: Dict[str, float] = dict() # 支付的详情信息存储
35 | for method, fee in payments.items():
36 | items[str(method)] = fee
37 |
38 | self.append(settings.ExtraInformation.Payments, items)
39 |
40 |
41 | class PayingSuccess(fsm.Event):
42 | """支付平台通知支付成功"""
43 | description: str = settings.Description.PayingSuccess
44 |
45 | def action(self, payid: str, fee: float) -> NoReturn:
46 | """记录支付单的id"""
47 | self.append(settings.ExtraInformation.PayId, payid)
48 | self.append(settings.ExtraInformation.PayFee, fee)
49 |
50 |
51 | class PayingFailed(fsm.Event):
52 | """支付平台通知支付失败"""
53 | description: str = settings.Description.PayingFailed
54 |
55 | def action(self, payid: str, reason: str) -> NoReturn:
56 | """记录支付失败的原因"""
57 | self.append(settings.ExtraInformation.PayId, payid)
58 | self.append(settings.ExtraInformation.PayFailReason, reason)
59 |
60 |
61 | class OrderRetry(fsm.Event):
62 | """订单重试"""
63 | description: str = settings.Description.OrderRetry
64 |
65 |
66 | class OrderTimedOut(fsm.Event):
67 | """订单超时, 系统关闭订单"""
68 | description: str = settings.Description.OrderTimedOut
69 |
70 |
71 | class Shipped(fsm.Event):
72 | """商品已经交付快递"""
73 | description: str = settings.Description.Shipped
74 |
75 | def action(self, shipping) -> NoReturn:
76 | """记录物流单号"""
77 | self.append(settings.ExtraInformation.ShipInfo, shipping)
78 |
79 |
80 | class Delieverd(fsm.Event):
81 | """收到物流平台消息商品已经被送达"""
82 | description: str = settings.Description.Delieverd
83 |
84 |
85 | class Recieved(fsm.Event):
86 | """用户主动确认收货"""
87 | description: str = settings.Description.Recieved
88 |
89 |
90 | class RecieveTimingExcced(fsm.Event):
91 | """超时系统自动确认收货"""
92 | description: str = settings.Description.RecieveTimingExcced
93 |
94 |
95 | class RequestRefund(fsm.Event):
96 | """用户申请退款"""
97 | description: str = settings.Description.RequestRefund
98 |
99 | def action(self, reason: str) -> NoReturn:
100 | """记录用户申请退款的原因并下发申请单号"""
101 | refundid = encrypt.uuid()
102 | self.append(settings.ExtraInformation.RefundReason, reason)
103 | self.append(settings.ExtraInformation.RefundNumber, refundid)
104 |
105 |
106 | class RefundDenied(fsm.Event):
107 | """退款审核未通过"""
108 | description: str = settings.Description.RefundDenied
109 |
110 | def action(self, reason: str) -> NoReturn:
111 | """记录退款审核未通过的原因"""
112 | self.append(settings.ExtraInformation.RefundDenyReason, reason)
113 |
114 |
115 | class RefundApproved(fsm.Event):
116 | """退款审核已经通过, 等待支付平台处理退款"""
117 | description: str = settings.Description.RefundApproved
118 |
119 |
120 | class RefundSuccess(fsm.Event):
121 | """退款已经完成"""
122 | description: str = settings.Description.RefundSuccess
123 |
124 |
125 | class RefundFailed(fsm.Event):
126 | """退款失败"""
127 | description: str = settings.Description.RefundFailed
128 |
129 | def action(self, reason: str) -> NoReturn:
130 | """记录退款失败信息"""
131 | self.append(settings.ExtraInformation.RefundFailReason, reason)
132 |
--------------------------------------------------------------------------------
/leaf/selling/order/manager.py:
--------------------------------------------------------------------------------
1 | """
2 | 使用有限状态机算法
3 | 通过已经创建的事件和状态
4 | 对订单状态进行自动的调度
5 | """
6 | # pylint: disable=arguments-differ
7 |
8 | from typing import NoReturn
9 | from ...core.tools import web
10 | from ...core.algorithm import fsm
11 |
12 | from . import events
13 | from . import status
14 | from . import settings
15 |
16 | # 状态转移表
17 | _TransferTable = (
18 | (status.Created, events.Confirm, status.Confirmed),
19 | (status.Created, events.UserClose, status.Closed),
20 | (status.Created, events.OrderTimedOut, status.Closed),
21 | (status.Confirmed, events.Paying, status.Paying),
22 | (status.Confirmed, events.UserClose, status.Closed),
23 | (status.Confirmed, events.OrderTimedOut, status.Closed),
24 | (status.Paying, events.PayingSuccess, status.Paid),
25 | (status.Paying, events.PayingFailed, status.PayFailed),
26 | (status.PayFailed, events.OrderTimedOut, status.Closed),
27 | (status.PayFailed, events.OrderRetry, status.Created),
28 | (status.Paid, events.Shipped, status.Shipping),
29 | (status.Shipping, events.Delieverd, status.Delieverd),
30 | (status.Delieverd, events.Recieved, status.Completed),
31 | (status.Delieverd, events.RecieveTimingExcced, status.Completed),
32 | (status.Completed, events.RequestRefund, status.RefundReviewing),
33 | (status.RefundReviewing, events.RefundApproved, status.Refunding),
34 | (status.RefundReviewing, events.RefundDenied, status.Completed),
35 | (status.Refunding, events.RefundSuccess, status.Closed),
36 | (status.Refunding, events.RefundFailed, status.Completed)
37 | )
38 |
39 |
40 | class StatusManager(fsm.Machine):
41 | """创建一个订单状态管理器"""
42 |
43 | def __init__(self, orderid: str) -> NoReturn:
44 | """
45 | 订单状态管理器构造函数:
46 | 0. 根据订单id对管理器命名
47 | 1. 初始化状态转移对应表
48 | 2. 初始化进入状态
49 | """
50 | super().__init__(str(orderid))
51 |
52 | # 初始化状态转移对应表
53 | for record in _TransferTable:
54 | self.add(*record)
55 |
56 | def start(self) -> NoReturn:
57 | """开始从订单创建开始"""
58 | super().start(status.Created)
59 |
60 | def json(self) -> str:
61 | """将当前状态信息导出为 JSON 字串"""
62 | if not self.current is None:
63 | current = {
64 | settings.Status.Key.Code: self.current.code,
65 | settings.Status.Key.Description: self.current.description,
66 | settings.Status.Key.Extra: self.current.extra
67 | } # 当前状态导出字典
68 | else:
69 | current = None
70 |
71 | mapping = map(lambda event: {
72 | settings.Events.Key.Time: event.time,
73 | settings.Events.Key.OperationCode: event.opcode,
74 | settings.Events.Key.Extra: event.extra,
75 | settings.Events.Key.Description: event.description
76 | }, self.events) # 事件记录器导出为字典
77 |
78 | return web.JSONcreater({
79 | settings.Manager.Key.Name: self.name,
80 | settings.Manager.Key.CurrentStat: current,
81 | settings.Manager.Key.EventsRecorder: list(mapping)
82 | })
83 |
--------------------------------------------------------------------------------
/leaf/selling/order/order.py:
--------------------------------------------------------------------------------
1 | """订单类数据库模型"""
2 |
3 | import pickle
4 | from typing import List, NoReturn
5 | import mongoengine
6 |
7 | from . import events
8 | from . import manager as _man
9 | from .. import error
10 | from .. import settings
11 | from ..commodity import stock
12 |
13 | from ...core import modules
14 | from ...core import schedule
15 | from ...core.algorithm import fsm
16 |
17 |
18 | class Order(mongoengine.Document):
19 | """
20 | 订单类数据库模型:
21 | amount: 订单总金额 - float
22 | goods: 购买商品列表 - List[CacheedRef]
23 | status: 当前状态JSON字串 - str
24 | instance: 订单状态管理器的 pickle 对象 - bytes
25 | """
26 | amount = mongoengine.FloatField(min_value=0, required=True)
27 | currency = mongoengine.StringField(
28 | default=settings.General.DefaultCurrency)
29 |
30 | goods = mongoengine.ListField(
31 | mongoengine.CachedReferenceField(stock.Stock))
32 | instance = mongoengine.BinaryField(default=bytes)
33 |
34 | @staticmethod
35 | def create(goods: List[stock.Stock],
36 | timeout: int = settings.Order.OrderTimeout):
37 | """根据给定的商品列表创建一个订单类实例后入库"""
38 |
39 | # 检查列表是否为空
40 | if not goods:
41 | raise error.EmptyOrder(str(goods))
42 |
43 | # 检查商品是否都在售
44 | for good in goods:
45 | if not good.onsale:
46 | raise error.DiscontinueStock(good.id)
47 |
48 | # 检查商品设置的货币类型是否相同
49 | currencies = [good.currency for good in goods]
50 | if len(set(currencies)) != 1:
51 | raise error.DiffrentCurrencies(str(currencies))
52 |
53 | # 检查商品是否还有库存
54 | inventories = [good.inventory for good in goods]
55 | if not min(inventories):
56 | _iid = goods[inventories.index(0)].id
57 | raise error.InsufficientInventory(_iid)
58 |
59 | # 目标对象库存减少一
60 | for good in goods:
61 | good.inventory -= 1
62 | good.save()
63 |
64 | currency = currencies.pop()
65 | amount = sum([good.price for good in goods])
66 | order = Order(amount=amount, currency=currency, goods=goods)
67 | order.save()
68 |
69 | # pylint: disable=no-member
70 | instance = _man.StatusManager(order.id)
71 | instance.start()
72 | order.instance = pickle.dumps(instance)
73 | order.save()
74 |
75 | # 设置订单超时定时器
76 | def __cancel_order():
77 | """取消当前订单"""
78 | cancellation = events.OrderTimedOut()
79 | order.manager.handle(cancellation)
80 | timer = schedule.Worker(__cancel_order, timeout, 1, False)
81 | schedule_manager: schedule.Manager = modules.schedules
82 | schedule_manager.start(timer)
83 | return order
84 |
85 | @property
86 | def manager(self):
87 | """返回当前订单类的 manager 实例代理"""
88 | if not self.instance:
89 | return None
90 | man: _man.StatusManager = pickle.loads(self.instance)
91 | return ManagerProxy(self, man)
92 |
93 |
94 | class ManagerProxy:
95 | """
96 | 代理Manager的访问
97 | add 函数不代理 - 实例化后的状态机不应该改变状态图
98 | """
99 |
100 | def __init__(self, db: Order, instance: _man.StatusManager):
101 | """实例化代理对象"""
102 | self._db, self._instance = db, instance
103 |
104 | def json(self):
105 | """代理 json 提取操作"""
106 | return self._instance.json()
107 |
108 | @property
109 | def name(self) -> str:
110 | """返回名称"""
111 | return self._instance.name
112 |
113 | @property
114 | def current(self) -> fsm.State:
115 | """返回当前状态"""
116 | return self._instance.current
117 |
118 | @property
119 | def events(self) -> List[fsm.Event]:
120 | """代理 events 操作"""
121 | return self._instance.events
122 |
123 | def stop(self) -> NoReturn:
124 | """代理 stop 操作"""
125 | self._instance.stop()
126 | self._db.instance = pickle.dumps(self._instance)
127 | self._db.save()
128 |
129 | def start(self) -> NoReturn:
130 | """代理 start 操作"""
131 | self._instance.start()
132 | self._db.instance = pickle.dumps(self._instance)
133 | self._db.save()
134 |
135 | def handle(self, event: fsm.Event) -> NoReturn:
136 | """代理事件处理操作"""
137 | self._instance.handle(event)
138 | self._db.instance = pickle.dumps(self._instance)
139 | self._db.save()
140 |
--------------------------------------------------------------------------------
/leaf/selling/order/settings.py:
--------------------------------------------------------------------------------
1 | """订单相关设置"""
2 |
3 |
4 | class Events:
5 | """事件相关设置"""
6 |
7 | class Key:
8 | """导出键设置"""
9 | OperationCode = "code" # 操作码
10 | Time = "time" # 发生时间
11 | Description = "description" # 描述
12 | Extra = "extra" # 额外信息
13 |
14 | class ExtraInformation:
15 | """额外信息描述键"""
16 | CloseReason = "订单关闭原因"
17 | Payments = "支付详情"
18 | PayId = "支付单号"
19 | PayFee = "实际支付金额"
20 | PayFailReason = "支付失败原因"
21 | ShipInfo = "物流信息"
22 | RefundReason = "退款原因"
23 | RefundNumber = "退款单号"
24 | RefundDenyReason = "退款申请未通过原因"
25 | RefundFailReason = "退款在支付平台处理失败原因"
26 |
27 | class Description:
28 | """描述设置"""
29 | Confirm = "用户已经确认了订单, 等待支付"
30 | UserClose = "用户主动关闭订单"
31 | Paying = "用户正在付款, 等待支付结果"
32 | PayingSuccess = "用户已支付成功"
33 | PayingFailed = "用户支付失败"
34 | OrderRetry = "支付失败, 转入重试"
35 | OrderTimedOut = "订单超时, 系统关闭订单"
36 | Shipped = "商品已经交付物流"
37 | Delieverd = "商品已经送达"
38 | Recieved = "用户确认收货"
39 | RecieveTimingExcced = "超时系统自动确认收货"
40 | RequestRefund = "用户申请退款"
41 | RefundDenied = "用户退款申请已拒绝"
42 | RefundApproved = "用户退款申请已通过, 等待支付平台处理退款"
43 | RefundSuccess = "支付平台退款完成"
44 | RefundFailed = "支付平台退款失败"
45 |
46 |
47 | class Status:
48 | """订单状态相关设置"""
49 |
50 | class Key:
51 | """导出键设置"""
52 | Code = "code" # 状态码
53 | Description = "description" # 说明
54 | Extra = "extra" # 额外信息
55 |
56 | class ExtraInfomation:
57 | """额外信息补充描述"""
58 | Reason = "原因"
59 | Operator = "操作者"
60 | OperateTime = "操作时间"
61 | ShipDetails = "物流详细信息"
62 |
63 | class Description:
64 | """订单状态描述"""
65 | Created = "订单创建"
66 | Confirmed = "订单已经被确认, 等待用户支付"
67 | Paying = "用户正在支付, 等待支付结果"
68 | Paid = "用户支付成功, 等待发货"
69 | PayFailed = "用户支付失败, 尝试重新支付"
70 | Shipping = "订单已经交付物流运送"
71 | Delieverd = "订单物流已经完成"
72 | RefundReviewing = "退款单正在审核"
73 | Refunding = "退款已经交付支付平台处理"
74 | Completed = "订单已经完成"
75 | Closed = "订单已关闭"
76 |
77 |
78 | class Manager:
79 | """订单状态管理器相关设置"""
80 |
81 | class Key:
82 | """导出键设置"""
83 | Name = "orderid" # 状态机名称 - 订单号
84 | CurrentStat = "current" # 当前状态
85 | EventsRecorder = "events" # 事件记录器
86 |
--------------------------------------------------------------------------------
/leaf/selling/order/status.py:
--------------------------------------------------------------------------------
1 | """订单的所有状态"""
2 | # pylint: disable=signature-differs
3 | # # pylint: disable=arguments-differ
4 |
5 | from ...core.algorithm import fsm
6 |
7 | from . import events
8 | from .settings import Status as settings
9 |
10 | # 状态 - 订单已经创建
11 | Created = fsm.State(0, settings.Description.Created)
12 | Created.add(events.Confirm)
13 | Created.add(events.UserClose)
14 |
15 | # 状态 - 用户开始支付
16 | Confirmed = fsm.State(1, settings.Description.Confirmed)
17 | Confirmed.add(events.Paying)
18 |
19 |
20 | class _Paying(fsm.State):
21 | """状态 - 用户正在支付"""
22 |
23 | def enter(self, payments: fsm.Event):
24 | """进入正在支付状态 - 保存支付详情"""
25 | self.extra = payments.extra
26 |
27 |
28 | Paying = _Paying(2, settings.Description.Paying)
29 | Paying.add(events.PayingSuccess)
30 | Paying.add(events.PayingFailed)
31 |
32 |
33 | class _Paid(fsm.State):
34 | """状态 - 订单支付成功"""
35 |
36 | def enter(self, payid: fsm.Event):
37 | """进入支付成功状态 - 保存支付单详情"""
38 | self.extra = payid.extra
39 |
40 |
41 | Paid = _Paid(3, settings.Description.Paid)
42 | Paid.add(events.Shipped)
43 |
44 |
45 | class _PayFailed(fsm.State):
46 | """状态 - 用户支付失败"""
47 |
48 | def enter(self, reason: fsm.Event):
49 | """进入支付失败状态 - 保存支付失败原因"""
50 | self.extra = reason.extra
51 |
52 |
53 | PayFailed = _PayFailed(4, settings.Description.PayFailed)
54 | PayFailed.add(events.OrderRetry)
55 | PayFailed.add(events.OrderTimedOut)
56 |
57 |
58 | class _Shipping(fsm.State):
59 | """状态 - 订单正在运送"""
60 |
61 | def enter(self, reason: fsm.Event):
62 | """进入运送状态保存运单信息并创建详细信息列表"""
63 | self.extra = reason.extra
64 | self.extra[settings.ExtraInfomation.ShipDetails] = list()
65 |
66 |
67 | Shipping = _Shipping(5, settings.Description.Shipping)
68 | Shipping.add(events.Delieverd)
69 |
70 |
71 | # 状态 - 订单已签收
72 | Delieverd = fsm.State(6, settings.Description.Delieverd)
73 | Delieverd.add(events.RecieveTimingExcced)
74 | Delieverd.add(events.Recieved)
75 |
76 |
77 | # 状态 - 订单完成
78 | Completed = fsm.State(7, settings.Description.Completed)
79 | Completed.add(events.RequestRefund)
80 |
81 |
82 | class _RefundReviewing(fsm.State):
83 | """状态 - 退款正在审核"""
84 |
85 | def enter(self, reason: fsm.Event):
86 | """
87 | 进入退款审核状态时需要采集:
88 | 0. 退款申请原因
89 | 1. 退款申请事件生成的退款申请码
90 | """
91 | self.extra = reason.extra
92 |
93 |
94 | RefundReviewing = fsm.State(8, settings.Description.RefundReviewing)
95 | RefundReviewing.add(events.RefundApproved)
96 | RefundReviewing.add(events.RefundDenied)
97 |
98 |
99 | Refunding = fsm.State(9, settings.Description.Refunding)
100 | Refunding.add(events.RefundSuccess)
101 | Refunding.add(events.RefundFailed)
102 |
103 |
104 | class _Closed(fsm.State):
105 | """状态 - 订单关闭"""
106 |
107 | def enter(self, reason: fsm.Event):
108 | """
109 | 进入订单关闭时检测进入原因:
110 | 0. 如果是用户主动关闭则记录关闭原因
111 | 1. 如果是发生了订单超时事件则记录为订单超时
112 | 2. 如果是退款则记录订单已经完成退款
113 | """
114 | self.extra[settings.ExtraInfomation.OperateTime] = reason.time
115 | self.extra[settings.ExtraInfomation.Reason] = reason.description
116 | if reason == events.UserClose:
117 | self.extra = reason.extra
118 |
119 |
120 | Closed = _Closed(10, settings.Description.Closed)
121 |
--------------------------------------------------------------------------------
/leaf/selling/settings.py:
--------------------------------------------------------------------------------
1 | """产品与销售相关设置"""
2 |
3 | from ..core.schedule import MINUTE
4 | from ..core.schedule import HOUR
5 |
6 |
7 | class General:
8 | """与交易相关的全局设置"""
9 |
10 | DefaultCurrency = "CNY" # 默认的货币类型
11 | AllowCurrency = {
12 | "CNY", "USD", "RUB"
13 | } # 允许设置的价格单位
14 |
15 |
16 | class Order:
17 | """订单相关的设置项目"""
18 |
19 | OrderTimeout = 30 * MINUTE # 默认订单超时时间为30min
20 |
21 |
22 | class Product:
23 | """产品相关的设置"""
24 |
25 | TagsCacheTime = 3 * HOUR # 查询全部标签的缓存有效时间
26 |
--------------------------------------------------------------------------------
/leaf/views/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Leaf 框架 view 层蓝图管理:
3 | rbac - 用户权限控制框架蓝图
4 | wxpay - 微信支付回调函数蓝图
5 | weixin - 微信公众平台回调函数蓝图
6 | plugins - 插件管理及功能支持蓝图
7 | """
8 |
9 | from . import rbac
10 | from . import wxpay
11 | from . import weixin
12 | from . import plugins
13 |
--------------------------------------------------------------------------------
/leaf/views/commodity/__init__.py:
--------------------------------------------------------------------------------
1 | """关于商品与销售相关的视图函数"""
2 |
3 | # pylint: disable=wrong-import-position
4 |
5 | commodity = __import__("flask").Blueprint("commodity", __name__)
6 |
7 | from . import product
8 | from . import stock
9 |
--------------------------------------------------------------------------------
/leaf/views/order.py:
--------------------------------------------------------------------------------
1 | """订单相关的接口与事件绑定"""
2 |
3 | import logging
4 | from typing import NoReturn
5 |
6 | from flask import Blueprint
7 |
8 | from ..core import modules
9 | from ..core import events as _events
10 | from ..core.error import Error as _Error
11 |
12 | from ..selling import error
13 | from ..selling.order import Order
14 | from ..selling.order import events
15 |
16 | order = Blueprint("order", __name__)
17 |
18 | # 注册支付成功与失败及退款提醒事件
19 | # 位置参数列表 - openid, transaction id, trade_out_no, cash_fee
20 | logger = logging.getLogger("leaf.selling.order")
21 |
22 |
23 | def _paysucess_handler(_openid: str, tactid: str, orderid: str, amount: float) -> NoReturn:
24 | """支付成功通知处理"""
25 | # pylint: disable=no-member
26 | _order = Order.objects(id=orderid)
27 |
28 | try:
29 | # 被通知到的订单不存在 - 记录错误(严重错误)
30 | if not _order:
31 | raise error.InvalidPaymentCallback(
32 | orderid + ' - ' + str(amount))
33 | _order: Order = _order.pop()
34 |
35 | # 生成一个支付成功的有限状态机事件
36 | paid = events.PayingSuccess()
37 | paid.action(tactid, amount)
38 | _order.manager.handle(paid)
39 |
40 | except _Error as _error:
41 | logger.exception(_error)
42 |
43 |
44 | def _payfail_handler(_openid: str, tactid: str, orderid: str,
45 | amount: float, reason: str) -> NoReturn:
46 | """支付失败通知处理"""
47 | # pylint: disable=no-member
48 | _order = Order.objects(id=orderid)
49 |
50 | try:
51 | # 被通知到的订单不存在 - 记录错误(严重错误)
52 | if not _order:
53 | raise error.InvalidPaymentCallback(
54 | orderid + ' - ' + str(amount))
55 | _order: Order = _order.pop()
56 |
57 | # 生成一个支付失败的有限状态机事件
58 | payfailed = events.PayingFailed()
59 | payfailed.action(tactid, reason)
60 | _order.manager.handle(payfailed)
61 |
62 | except _Error as _error:
63 | logger.exception(_error)
64 |
65 |
66 | _wxpay_paysuccess: _events.Event = modules.events.event(
67 | "leaf.payments.wxpay.notify.pay.success")
68 | _wxpay_payfail: _events.Event = modules.events.event(
69 | "leaf.payments.wxpay.notify.pay.fail")
70 |
71 | _wxpay_paysuccess.hook(_paysucess_handler)
72 | _wxpay_payfail.hook(_payfail_handler)
73 | # _wxpay_refund.hook() # 暂时不启用
74 |
--------------------------------------------------------------------------------
/leaf/views/plugins.py:
--------------------------------------------------------------------------------
1 | """对插件路由的支持"""
2 |
3 | from typing import Iterable
4 |
5 | from flask import g
6 | from flask import abort
7 | from flask import request
8 | from flask import Response
9 | from flask import Blueprint
10 |
11 | from ..core import modules
12 | from ..core.abstract.plugin import Plugin
13 | from ..plugins import Manager
14 |
15 | # 生成蓝图
16 | plugins = Blueprint("plugins", __name__)
17 |
18 | # 所有 HTTP 方法
19 | __ALL_METHODS = [
20 | 'GET', 'HEAD', 'POST', 'PUT',
21 | 'DELETE', 'CONNECT', 'OPTIONS',
22 | 'TRACE', 'PATCH'
23 | ]
24 |
25 |
26 | def __para_converter(paras: dict, order: Iterable) -> tuple:
27 | """
28 | 根据给定的参数字典和顺序列表返回有序参数序列:
29 | __para_converter({
30 | "second": 2, "first": 1
31 | }, ("first", "second"))
32 |
33 | *注意: 当参数在 paras 中未找到时会以 None 代替 - 而不是引发 KeyError
34 | """
35 | result = tuple(paras.get(key) for key in order)
36 | return result
37 |
38 |
39 | # 定义插件的 after_request 函数支持
40 | @plugins.after_request
41 | def after_request(response: Response) -> Response:
42 | """
43 | 执行在 forward 函数中获取到的插件 after_request 函数
44 | """
45 | if g.success:
46 | g.plugin_func_after_request(response)
47 | return response
48 |
49 | # 定义插件的函数支持
50 | @plugins.route("/", methods=__ALL_METHODS)
51 | def forward(token: str) -> object:
52 | """
53 | 从给定的 Token 中寻找需要运行的插件
54 | 并将当前的的控制流移交给对应插件
55 | 获取到数据之后进行返回
56 |
57 | *访问的 url 示例:
58 | init.blog/plugins/accesstoken.get
59 | """
60 |
61 | g.success = False
62 |
63 | # 获取插件的访问主域以及 url
64 | domain, *urls = token.split(".")
65 | url: str = '/'.join(urls)
66 |
67 | # 当访问主页时经过上述处理会得到空字符串
68 | url = '/' if not url else url
69 |
70 | # 获取插件管理器根据 domain 寻找插件
71 | manager: Manager = modules.plugins
72 | plugin: Plugin = manager.domain(domain)
73 | if plugin is None:
74 | return abort(404)
75 |
76 | # 运行插件的 before_request 函数并设置 after_request
77 | plugin.get_before_request()()
78 | g.success = True
79 | g.plugin_func_after_request = plugin.get_after_request()
80 |
81 | # 获取视图函数以及其需要的参数
82 | handler, paramaters = plugin.find(url, request.method)
83 | order = plugin.paramaters(handler)
84 | paramaters = __para_converter(paramaters, order)
85 |
86 | # 将控制流交给插件
87 | return handler(*paramaters)
88 |
--------------------------------------------------------------------------------
/leaf/views/rbac/__init__.py:
--------------------------------------------------------------------------------
1 | """关于 RBAC 相关的视图函数"""
2 |
3 | # pylint: disable=wrong-import-position
4 |
5 | rbac = __import__("flask").Blueprint("rbac", __name__)
6 |
7 | from . import user
8 | from . import jwt
9 | from . import group
10 | from . import auth
11 | from . import accesspoint
12 |
--------------------------------------------------------------------------------
/leaf/views/rbac/accesspoint.py:
--------------------------------------------------------------------------------
1 | """AccessPoint 模型视图函数"""
2 |
3 | from typing import List, Set
4 |
5 | from flask import request
6 | from bson import ObjectId
7 | from mongoengine import NotUniqueError
8 |
9 | from . import rbac
10 | from ...api import wrapper
11 | from ...core.tools import web
12 |
13 | from ...rbac import error
14 | from ...rbac.model import AccessPoint
15 | from ...rbac.functions import user
16 | from ...rbac.functions import accesspoint as funcs
17 |
18 |
19 | @rbac.route("/accesspoints/", methods=["GET"])
20 | @wrapper.require("leaf.views.rbac.accesspoint.query")
21 | @wrapper.wrap("accesspoint")
22 | def query_accesspoint_byname(pointname: str) -> AccessPoint:
23 | """根据指定的名称查找相关的访问点信息"""
24 | point: AccessPoint = funcs.Retrieve.byname(pointname)
25 | return point
26 |
27 |
28 | @rbac.route("/accesspoints", methods=["GET"])
29 | @wrapper.require("leaf.views.rbac.accesspoint.get")
30 | @wrapper.wrap("accesspoints")
31 | def getall_accesspoints() -> List[AccessPoint]:
32 | """返回所有的访问点信息"""
33 | # pylint: disable=no-member
34 | return list(AccessPoint.objects)
35 |
36 |
37 | @rbac.route("/accesspoints/", methods=["DELETE"])
38 | @wrapper.require("leaf.views.rbac.accesspoint.delete")
39 | @wrapper.wrap("status")
40 | def delete_accesspoint(pointname: str) -> bool:
41 | """删除某一个访问点信息"""
42 | point: AccessPoint = funcs.Retrieve.byname(pointname)
43 | point.delete()
44 | return True
45 |
46 |
47 | @rbac.route("/accesspoints", methods=["POST"])
48 | @wrapper.require("leaf.views.rbac.accesspoint.create")
49 | @wrapper.wrap("accesspoint")
50 | def create_accesspoint() -> AccessPoint:
51 | """创建一个访问点信息"""
52 | pointname: str = request.form.get("pointname", type=str, default='')
53 | required: int = request.form.get("required", type=int, default=0)
54 | strict: bool = bool(request.form.get("strict", type=int, default=0))
55 | description: str = request.form.get("description", type=str, default='')
56 |
57 | try:
58 | point: AccessPoint = AccessPoint(pointname=pointname, required=required,
59 | strict=strict, description=description)
60 | return point.save()
61 | except NotUniqueError as _error:
62 | raise error.AccessPointNameConflicting(pointname)
63 |
64 |
65 | @rbac.route("/accesspoints/", methods=["PUT"])
66 | @wrapper.require("leaf.views.rbac.accesspoint.update")
67 | @wrapper.wrap("accesspoint")
68 | def update_accesspoint(pointname: str) -> AccessPoint:
69 | """更新某一个访问点信息"""
70 | required: int = request.form.get("required", type=int, default=0)
71 | strict: bool = bool(request.form.get("strict", type=int, default=0))
72 | description: str = request.form.get("description", type=str, default='')
73 |
74 | point: AccessPoint = funcs.Retrieve.byname(pointname)
75 | point.required = required
76 | point.strict = strict
77 | point.description = description
78 | point: AccessPoint = AccessPoint(pointname=pointname, required=required,
79 | strict=strict, description=description)
80 |
81 | return point.save()
82 |
83 |
84 | @rbac.route("/accesspoints//exceptions", methods=["PUT"])
85 | @wrapper.require("leaf.views.rbac.accesspoint.update")
86 | @wrapper.wrap("accesspoint")
87 | def set_exceptions_user_for_accesspoint(pointname: str) -> AccessPoint:
88 | """为指定的 AccessPoint 管理特权用户"""
89 | point: AccessPoint = funcs.Retrieve.byname(pointname)
90 | raw: List[str] = [str(user) for user in point.exceptions]
91 | new: List[str] = web.JSONparser(request.form.get("users"))
92 | diff: Set[ObjectId] = set(new) - set(raw)
93 |
94 | # 检查每一个用户是否都存在
95 | exceptions = list()
96 | for userid in diff:
97 | user.Retrieve.byid(userid)
98 | exceptions.append(ObjectId(userid))
99 |
100 | point.exceptions = exceptions
101 | return point.save()
102 |
--------------------------------------------------------------------------------
/leaf/views/rbac/auth.py:
--------------------------------------------------------------------------------
1 | """控制用户的认证相关数据"""
2 |
3 | from typing import List, Tuple
4 | from flask import request
5 |
6 | from . import rbac
7 |
8 | from ...api import wrapper
9 | from ...api import validator
10 |
11 | from ...rbac import error
12 | from ...rbac.model import UserIndex
13 | from ...rbac.functions import auth as authfuncs
14 | from ...rbac.functions import user as userfuncs
15 |
16 |
17 | @rbac.route("/auths//", methods=["POST"])
18 | @wrapper.require("leaf.views.rbac.auth.create", checkuser=True)
19 | @wrapper.wrap("status")
20 | def create_auth_with_user_index(userid: str, typeid: str) -> bool:
21 | """根据用户的某一个 index 创建 Auth 文档"""
22 | userid = validator.operator(userid)
23 | user = userfuncs.Retrieve.byid(userid)
24 | index: UserIndex = user.indexs.get(typeid=typeid)
25 | password: str = request.form.get("password", type=str)
26 | description: str = request.form.get("description", type=str,
27 | default=index.description)
28 | authfuncs.Create.withother(index.value, userid,
29 | password, description=description)
30 | return True
31 |
32 |
33 | @rbac.route("/auths//status", methods=["PUT"])
34 | @wrapper.require("leaf.views.rbac.atuh.update")
35 | @wrapper.wrap("status")
36 | def update_status_for_authdoc(index: str) -> bool:
37 | """更新用户的认证文档状态"""
38 | status: bool = request.form.get("status", type=bool, default=True)
39 | authdoc = authfuncs.Retrieve.byindex(index)
40 | authdoc.status = status
41 | authdoc.save()
42 | return True
43 |
44 |
45 | @rbac.route("/auths//", methods=["DELETE"])
46 | @wrapper.require("leaf.views.rbac.auth.delete", checkuser=True)
47 | @wrapper.wrap("status")
48 | def delete_authdoc(userid: str, index: str) -> bool:
49 | """删除用户的某一种认证方式"""
50 | userid = validator.operator(userid)
51 | authfuncs.Delete.byindex(userid, index)
52 | return True
53 |
54 |
55 | @rbac.route("/auths/", methods=["GET"])
56 | @wrapper.require("leaf.views.rbac.auth.get", checkuser=True)
57 | @wrapper.wrap("auths")
58 | def query_auth_map_with_userid(userid: str) -> List[Tuple[UserIndex, bool]]:
59 | """
60 | 查询用户的索引与认证文档的对应状态
61 | 返回类似下面的返回值:
62 | [(UserIndex1, True), (UserIndex2, False), ...]
63 | """
64 | userid = validator.operator(userid)
65 | user = userfuncs.Retrieve.byid(userid)
66 | indexs: List[UserIndex] = user.indexs
67 | mapping: List[Tuple[UserIndex, bool]] = list()
68 |
69 | for index in indexs:
70 | try:
71 | authfuncs.Retrieve.byindex(index.value)
72 | except error.AuthenticationNotFound as _error:
73 | mapping.append((index, False))
74 | continue
75 | else:
76 | mapping.append((index, True))
77 |
78 | return mapping
79 |
80 |
81 | @rbac.route("/auths//password", methods=["PUT"])
82 | @wrapper.require("leaf.views.rbac.auth.update", checkuser=True)
83 | @wrapper.wrap("status")
84 | def update_password(userid: str) -> bool:
85 | """更新用户密码"""
86 | userid = validator.operator(userid)
87 | current: str = request.form.get("current", type=str)
88 | new: str = request.form.get("new", type=str)
89 | authfuncs.Update.password(userid, current, new)
90 | return True
91 |
--------------------------------------------------------------------------------
/leaf/views/rbac/group.py:
--------------------------------------------------------------------------------
1 | """用户组管理视图函数"""
2 |
3 | from typing import List, Set
4 |
5 | from bson import ObjectId
6 | from flask import request
7 |
8 | from . import rbac
9 |
10 | from ...api import wrapper
11 | from ...api import validator
12 | from ...core.tools import web
13 | from ...rbac.model import Group
14 | from ...rbac.functions import group as funcs
15 | from ...rbac.functions import user as userfuncs
16 |
17 |
18 | @rbac.route("/groups/", methods=["GET"])
19 | @wrapper.require("leaf.views.rbac.group.query")
20 | @wrapper.wrap("group")
21 | def query_group_byid(groupid: str) -> Group:
22 | """根据给定的 id 查找用户组"""
23 | groupid = validator.objectid(groupid)
24 | return funcs.Retrieve.byid(groupid)
25 |
26 |
27 | @rbac.route("/groups", methods=["GET"])
28 | @wrapper.require("leaf.views.rbac.group.list")
29 | @wrapper.wrap("groups")
30 | def list_all_groups() -> List[Group]:
31 | """列出所有的用户组信息"""
32 | # pylint: disable=no-member
33 | return Group.objects
34 |
35 |
36 | @rbac.route("/groups/name/", methods=["GET"])
37 | @wrapper.require("leaf.views.rbac.group.query")
38 | @wrapper.wrap("groups")
39 | def query_group_byname(name: str) -> List[Group]:
40 | """根据名称查找指定的用户组"""
41 | # pylint: disable=no-member
42 | return Group.objects(name=name)
43 |
44 |
45 | @rbac.route("/groups/", methods=["DELETE"])
46 | @wrapper.require("leaf.views.rbac.group.delete")
47 | @wrapper.wrap("status")
48 | def delete_group(groupid: str) -> bool:
49 | """删除某一个特定的用户组"""
50 | groupid = validator.objectid(groupid)
51 | group: Group = funcs.Retrieve.byid(groupid)
52 | return group.delete()
53 |
54 |
55 | @rbac.route("/groups", methods=["POST"])
56 | @wrapper.require("leaf.views.rbac.group.add")
57 | @wrapper.wrap("group")
58 | def add_group() -> Group:
59 | """"增加一个用户组"""
60 | name: str = request.form.get("name", type=str, default='')
61 | description: str = request.form.get("description", type=str, default='')
62 | permission: int = request.form.get("permission", type=int, default=0)
63 | group: Group = Group(name=name, description=description, permission=permission)
64 | return group.save()
65 |
66 |
67 | @rbac.route("/groups/", methods=["PUT"])
68 | @wrapper.require("leaf.views.rbac.group.update")
69 | @wrapper.wrap("group")
70 | def update_group(groupid: str) -> Group:
71 | """更新某一个用户组的信息"""
72 | groupid = validator.objectid(groupid)
73 | group: Group = funcs.Retrieve.byid(groupid)
74 | name: str = request.form.get("name", type=str, default='')
75 | description: str = request.form.get("description", type=str, default='')
76 | permission: int = request.form.get("permission", type=int, default=0)
77 | group.name = name
78 | group.description = description
79 | group.permission = permission
80 | return group.save()
81 |
82 |
83 | @rbac.route("/groups//users", methods=["PUT"])
84 | @wrapper.require("leaf.views.rbac.group.edituser")
85 | @wrapper.wrap("group")
86 | def add_users_to_group(groupid: str) -> Group:
87 | """
88 | 编辑用户组中的用户:
89 | 计算所有增加的用户 - 对所有的增加用户进行加组操作
90 | 计算所有被移出组的用户 - 对所有的移出用户进行移出操作
91 | """
92 | groupid = validator.objectid(groupid)
93 | group: Group = funcs.Retrieve.byid(groupid)
94 | raw: List[str] = [str(user) for user in group.users]
95 | new: List[str] = web.JSONparser(request.form.get("users"))
96 |
97 | # 集合计算增加与移除的部分
98 | removed: Set[ObjectId] = set(raw) - set(new)
99 | added: Set[ObjectId] = set(new) - set(raw)
100 |
101 | # 给用户添加组信息
102 | for userid in added:
103 | userfuncs.Create.group(userid, groupid)
104 | for userid in removed:
105 | userfuncs.Delete.group(userid, groupid)
106 |
107 | # 返回更新之后的用户组信息
108 | return funcs.Retrieve.byid(groupid)
109 |
--------------------------------------------------------------------------------
/leaf/views/rbac/jwt.py:
--------------------------------------------------------------------------------
1 | """JWT 视图函数"""
2 |
3 | import logging
4 | from typing import List
5 |
6 | from flask import request
7 | from bson import ObjectId
8 |
9 | from . import rbac
10 |
11 | from ...api import wrapper
12 | from ...core import events
13 | from ...core import modules
14 |
15 | from ...rbac import jwt
16 | from ...rbac import error
17 | from ...rbac.model import Group
18 | from ...rbac.functions import user
19 | from ...rbac.functions import auth
20 |
21 | # 获取事件与日志管理器
22 | manager: events.Manager = modules.events
23 | logger = logging.getLogger("leaf.views.rbac.jwt")
24 |
25 | issued = events.Event("leaf.rbac.jwt.token.issued", ((ObjectId,), {}),
26 | description="签发了一个 JWT Token")
27 | failed = events.Event("leaf.rbac.jwt.token.authfailed", ((str, str), {}),
28 | description="在签发时遇到了验证错误")
29 | manager.add(issued)
30 | manager.add(failed)
31 |
32 |
33 | @rbac.route("/jwts/", methods=["POST"])
34 | @wrapper.wrap("token")
35 | def issue_jwt_token(usertoken: str) -> str:
36 | """为指定的用户颁发 JWT Token"""
37 | password: str = request.form.get("password", default=str(), type=str)
38 | validation = auth.Generator.valid(usertoken, password)
39 | if not validation:
40 | failed.notify(usertoken, password)
41 | raise error.AuthenticationError(usertoken)
42 |
43 | # 获取认证信息
44 | authdoc = auth.Retrieve.byindex(usertoken)
45 | if not authdoc.status:
46 | failed.notify(usertoken, password)
47 | raise error.AuthenticationDisabled(usertoken)
48 | userid: ObjectId = authdoc.user.id
49 | salt: str = authdoc.salt
50 |
51 | # 获取用户组权限
52 | userdoc = user.Retrieve.byid(userid)
53 | groups: List[Group] = userdoc.groups
54 | permissions = tuple((group.permission for group in groups))
55 |
56 | # 颁发 Token
57 | token = jwt.Token(salt)
58 | token.header()
59 | token.payload(issuer="leaf.views.jwt", audience=str(userid),
60 | other={jwt.settings.Payload.Permission: permissions})
61 | issued.notify(userid)
62 | return token.issue()
63 |
--------------------------------------------------------------------------------
/leaf/views/weixin.py:
--------------------------------------------------------------------------------
1 | """微信公众平台回调函数支持"""
2 |
3 | from flask import request
4 | from flask import Blueprint
5 |
6 | from ..core import modules
7 |
8 | from ..weixin import const
9 | from ..weixin import settings
10 | from ..weixin.reply import Event
11 | from ..weixin.reply import Message
12 |
13 | weixin = Blueprint("weixin", __name__)
14 |
15 |
16 | @weixin.route("/" + settings.Interface, methods=["GET", "POST"])
17 | def message_and_event_preducer():
18 | """处理微信发来的消息和事件"""
19 |
20 | # 微信消息加密与签名实例引用
21 | message_handler: Message = modules.weixin.message
22 | event_handler: Event = modules.weixin.event
23 |
24 | # 获取 url 参数和 post 参数
25 | message: str = request.data.decode()
26 | paramaters: dict = request.args.to_dict()
27 |
28 | # 是否为回显消息
29 | if const.Encrypt.URL.Key.Echo in paramaters.keys():
30 | return paramaters.get(const.Encrypt.URL.Key.Echo)
31 |
32 | # 判断是消息类型还是事件类型
33 | try:
34 | if const.Message.Message in message:
35 | reply = message_handler.reply(paramaters, message)
36 | return reply
37 |
38 | if const.Message.Event in message:
39 | event_handler.handle(message)
40 | return settings.Message.EmptyReply
41 |
42 | # 当超时错误之后返回自定义空消息
43 | except TimeoutError as _error:
44 | return settings.Message.EmptyReply
45 |
46 | return settings.Message.EmptyReply
47 |
--------------------------------------------------------------------------------
/leaf/views/wxpay.py:
--------------------------------------------------------------------------------
1 | """
2 | 一个基于微信支付的消息过滤器:
3 | 因为微信支付平台的回传信息是有一定规则的
4 | 所以可以在这一步根据规则检验数据包是否合法
5 | 如果不合法则可以 raise 相关的错误给业务层处理
6 |
7 | 同时, 这个过滤器也还可以用作 views 支付视图函数中
8 | 用来处理回调函数的信息过滤
9 | """
10 |
11 | import logging
12 | from xml.etree.ElementTree import ParseError
13 |
14 | from flask import Blueprint
15 | from flask import request
16 |
17 | from ..core import events
18 | from ..core import modules
19 | from ..core.tools import web
20 |
21 | from ..payments.wxpay import const
22 | from ..payments.wxpay import error
23 | from ..payments.wxpay import payment
24 | from ..payments.wxpay import settings
25 | from ..payments.wxpay import signature
26 |
27 | # 支付提醒的标准回复
28 | _STANDARD_REPLY = web.XMLcreater({"xml": {
29 | const.WXPayResponse.Status: const.WXPayResponse.Success,
30 | const.WXPayResponse.Message: const.WXPayResponse.Nothing
31 | }}, encoding=None)
32 |
33 |
34 | wxpay = Blueprint("wxpay", __name__)
35 | logger = logging.getLogger("leaf.views.wxpay")
36 |
37 |
38 | # 注册支付成功与失败提醒事件
39 | # 位置参数列表 - openid, payid, trade_out_no, cash_fee
40 | paysuccess = events.Event(
41 | "leaf.payments.wxpay.notify.pay.success",
42 | ((str, str, str, float), {}),
43 | "支付成功时的结果通知"
44 | )
45 |
46 | # 增加退款原因参数
47 | payfail = events.Event(
48 | "leaf.payments.wxpay.notify.pay.fail",
49 | ((str, str, str, float, str), {}),
50 | "支付失败时的结果通知"
51 | )
52 |
53 | refund = events.Event(
54 | "leaf.payments.wxpay.notify.refund.success",
55 | ((str, str, str, float), {}),
56 | "退款成功的结果通知"
57 | )
58 |
59 | manager: events.Manager = modules.events
60 | manager.add(paysuccess)
61 | manager.add(payfail)
62 | manager.add(refund)
63 |
64 |
65 | @wxpay.route("/refund_notify", methods=["POST"])
66 | def refund_notify_handler() -> str:
67 | """处理退款通知"""
68 | response = request.data.decode(encoding="utf-8", errors="ignore")
69 |
70 | # 暂未获取到官方文档, 在 logger 中打日志
71 | logger.info(response)
72 |
73 | return _STANDARD_REPLY
74 |
75 |
76 | @wxpay.route("/notify", methods=["POST"])
77 | def payment_notify_handler() -> str:
78 | """微信支付付款结果通知处理函数"""
79 | response = request.data.decode(encoding="utf-8", errors="ignore")
80 |
81 | # 防止异常发生
82 | try:
83 | response = web.XMLparser(response)
84 | except ParseError as _error:
85 | logger.info(response)
86 | return _STANDARD_REPLY
87 |
88 | response = response.get(const.WXPayAddress.XMLTag, {})
89 | sigtool: signature = modules.payments.wxpay.signature
90 |
91 | # 校验签名是否正确
92 | if not sigtool.verify(**response):
93 | try:
94 | raise error.WXPaySignatureError(str(response))
95 | except error.WXPayError as _error:
96 | logger.error(_error)
97 | return _STANDARD_REPLY
98 |
99 | # 获取需要的信息
100 | openid = response.get(const.WXPayBasic.OpenID)
101 | orderid = response.get(const.WXPayOrder.ID.In)
102 | fee = response.get(const.WXPaymentNotify.Fee)
103 | transcationid = response.get(const.WXPayBasic.TransactionID)
104 |
105 | kvparas = dict()
106 | for key, value in settings.PaymentNotify.Keys.items():
107 | kvparas[value] = response.get(key)
108 |
109 | # 检测业务状态
110 | result = response.get(const.WXPaymentNotify.Status)
111 |
112 | # 支付失败
113 | if result != const.WXPayResponse.Success:
114 | reason = response.get(const.WXPaymentNotify.Error.Description)
115 | payfail.notify(openid, transcationid, orderid, fee, reason, **kvparas)
116 | return _STANDARD_REPLY
117 |
118 | fee = payment.currency_reverter(int(fee))
119 | paysuccess.notify(openid, transcationid, orderid, fee, **kvparas)
120 |
121 | return _STANDARD_REPLY
122 |
--------------------------------------------------------------------------------
/leaf/weixin/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台支持模块:
3 | encrypt - 微信公众平台消息体加密函数
4 | const - 微信公众平台相关常量定义
5 | apis - 微信公众平台API接口实现集
6 | settings - 微信相关设置
7 | """
8 |
9 | from . import apis
10 | from . import const
11 | from . import reply
12 | from . import settings
13 | from . import accesstoken
14 |
15 | from .encrypt import Encrypt
16 |
--------------------------------------------------------------------------------
/leaf/weixin/accesstoken/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Leaf 微信公众平台AccessToken管理组件
3 | """
4 |
5 | from .patch import Patcher
6 | from . import error
7 | from . import events
8 | from . import settings
9 |
--------------------------------------------------------------------------------
/leaf/weixin/accesstoken/const.py:
--------------------------------------------------------------------------------
1 | """
2 | AccessToken 获取常量定义
3 | """
4 |
5 | # 请求中携带的参数键
6 | GRANT_TYPE = "grant_type"
7 | APPID = "appid"
8 | SECRET = "secret"
9 | TYPE = "client_credential"
10 |
11 | # 响应中携带的参数键
12 | TOKEN = "access_token"
13 | EXPIRES = "expires_in"
14 |
15 | # 错误响应中的键
16 | CODE = "errcode"
17 | MESSAGE = "errmsg"
18 |
--------------------------------------------------------------------------------
/leaf/weixin/accesstoken/error.py:
--------------------------------------------------------------------------------
1 | """
2 | AccessToken 获取插件错误类定义
3 | """
4 |
5 | from ...core import error as _oerror
6 |
7 | Error = _oerror.Error
8 |
9 |
10 | class AConnectionError(Error):
11 | """获取过程中的网络错误"""
12 | code = 14101
13 | description = "在获取过程中遇到网络错误"
14 |
15 |
16 | class InvalidResponse(Error):
17 | """当收到了非法的响应时给出"""
18 | code = 14202
19 | description = "获取到了不正确的响应 - 留意中间人攻击"
20 |
21 |
22 | class ReachedMaxRetries(Error):
23 | """当达到了最大的重试次数时"""
24 | code = 14203
25 | description = "经过多次重试后仍无法正常更新 AccessToken - 插件将停止运行"
26 |
27 |
28 | class NoCachedAccessToken(Error):
29 | """暂时没有缓存的 AccessToken"""
30 | code = 14204
31 | description = "暂时没有缓存的 AccessToken"
32 |
33 |
34 | class InvalidAppID(Error):
35 | """不正确的 APPID"""
36 | code = 14013
37 | description = "不正确的 APPID"
38 |
39 |
40 | class InvalidAppSecret(Error):
41 | """不正确的 AppSecret"""
42 | code = 14125
43 | description = "不正确的 AppSecret"
44 |
45 |
46 | class IPLimited(Error):
47 | """IP 地址被限制"""
48 | code = 14164
49 | description = "请在公众平台添加当前 IP 到白名单"
50 |
51 |
52 | class AccessTokenStatusError(Error):
53 | """当前的 AccessToken 状态不正常时抛出"""
54 | code = 14205
55 | description = "缓存的 AccessToken 状态异常"
56 |
--------------------------------------------------------------------------------
/leaf/weixin/accesstoken/events.py:
--------------------------------------------------------------------------------
1 | """
2 | 注册插件事件
3 | """
4 |
5 | from ...core import events
6 |
7 | # 注册更新事件与停止事件
8 | updated = events.Event("leaf.weixin.accesstoken.updated", ((str, int), {}), "Accesstoken 被更新")
9 | failed = events.Event("leaf.weixin.accesstoken.failed", ((), {}), "Accesstoken 更新失败")
10 | stopped = events.Event("leaf.weixin.accesstoken.stopped", ((), {}), "Accesstoken 插件停止运行")
11 |
--------------------------------------------------------------------------------
/leaf/weixin/accesstoken/patch.py:
--------------------------------------------------------------------------------
1 | """AccessToken 获取与缓存器"""
2 |
3 | import json
4 | import logging
5 | import time as _t
6 | from typing import NoReturn
7 |
8 | from . import error
9 | from . import const
10 | from . import settings
11 |
12 | from ...core import modules
13 | from ...core import schedule
14 | from ...core.tools import web
15 | from ...core.tools import time
16 | from ...core.error import Messenger
17 |
18 |
19 | logger = logging.getLogger("leaf.weixin.accesstoken")
20 |
21 |
22 | class Patcher:
23 | """
24 | Leaf 微信公众平台 AccessToken 获取插件:
25 | get - 获取当前已经缓存的 Token
26 | stop - 停止任务的继续运行
27 | update - 手动更新一次 ACToken 缓存
28 | """
29 |
30 | def __init__(self, appid: str, secret: str):
31 | """
32 | 获取器构造函数:
33 | appid - 微信公众平台提供的公众号 AppId
34 | secret - 微信公众平台提供的 AppSecret
35 | 通过创建 leaf 的计划任务来进行更新
36 | """
37 | self.__status = False
38 |
39 | # 保存 APPID SECRET
40 | self.__appid = appid
41 | self.__secret = secret
42 |
43 | # 已经保存的缓存与过期时间
44 | self.__cache = str()
45 | self.__expire = 0
46 |
47 | # 设置 schedule 模块进行自动更新
48 | self.__work: schedule.Worker = schedule.Worker(
49 | self._do, settings.Gap)
50 |
51 | def set(self, cache: str, expire: int) -> NoReturn:
52 | """手动设置当前的 AccessToken 信息"""
53 | self.__cache = cache
54 | self.__expire = expire + time.now()
55 |
56 | def start(self):
57 | """开始任务执行"""
58 | manager: schedule.Manager = modules.schedules
59 | manager.start(self.__work)
60 | self.__status = True
61 |
62 | def _do(self, retires: int = settings.MaxRetries) -> NoReturn:
63 | """
64 | 经过包装的 update 函数
65 | 有一个默认参数 retires = settings.MaxRetires
66 | 当 update 执行遇到错误时则将 retries 减一递归
67 | 当 retires = 0 时停止任务运行
68 | """
69 | _t.sleep(settings.Pre)
70 | if retires == 0:
71 | self.stop()
72 | return
73 |
74 | try:
75 | self.update()
76 | except error.Error as _error:
77 | logger.error(_error)
78 | self._do(retires - 1)
79 |
80 | def update(self) -> str:
81 | """
82 | AccessToken 插件获取函数
83 | 通过 web.get 更新 ACToken 的值并进行更新
84 | 当遇到错误之后进行指定次数的重试
85 | """
86 |
87 | request = {
88 | const.GRANT_TYPE: const.TYPE,
89 | const.APPID: self.__appid,
90 | const.SECRET: self.__secret
91 | } # 请求的 GET 参数
92 |
93 | data, response = web.get(settings.Address, request)
94 |
95 | # 遇到网络错误
96 | if response.code != 200:
97 | failed = modules.events.event("leaf.weixin.accesstoken.failed")
98 | failed.boardcast()
99 | raise error.AConnectionError(response.code)
100 |
101 | # 尝试解析数据
102 | try:
103 | info: dict = web.JSONparser(data)
104 | token: str = info[const.TOKEN]
105 | expires: int = int(info[const.EXPIRES])
106 | except json.decoder.JSONDecodeError as _error:
107 | raise error.InvalidResponse(_error)
108 | except KeyError as _error:
109 | # 这时微信平台返回了异常提示
110 | code = int(info.get(const.CODE, 0))
111 | message = info.get(const.MESSAGE, '')
112 | raiser: Messenger = modules.error.messenger
113 | raise raiser.error(code, message=message)
114 |
115 | # 更新缓存 - 由广播通知的 update 事件处理
116 | # self.__cache = token
117 | # self.__expire = expires + time.now()
118 |
119 | # 发送事件通知
120 | updated = modules.events.event("leaf.weixin.accesstoken.updated")
121 | updated.boardcast(token, expires)
122 |
123 | return self.__cache
124 |
125 | def stop(self) -> NoReturn:
126 | """
127 | 停止获取任务
128 | 发送任务停止通知
129 | """
130 | if self.__status is False:
131 | return
132 |
133 | self.__work.stop()
134 | self.__status = False
135 |
136 | stopped = modules.events.event("leaf.weixin.accesstoken.stopped")
137 | stopped.boardcast()
138 |
139 | def restart(self) -> NoReturn:
140 | """
141 | 先设置任务停止
142 | 之后重启任务
143 | """
144 | # 这里不能调用 self.stop
145 | # 函数会触发事件通知使得插件状态失效
146 | self.__work.stop()
147 | self.__work.start()
148 |
149 | @property
150 | def status(self) -> bool:
151 | """返回当前更新器是否在工作"""
152 | return self.__status
153 |
154 | def get(self) -> str:
155 | """
156 | 返回缓存的 APPID
157 | """
158 | if self.__expire == 0 and not self.__status:
159 | raise error.ReachedMaxRetries("经过多次失败之后, 更新器已经停止工作")
160 |
161 | if self.__expire == 0:
162 | raise error.AccessTokenStatusError("正在获取token, 请稍后再试")
163 |
164 | if time.now() > self.__expire:
165 | raise error.AccessTokenStatusError("当前缓存已经超时且无法更新")
166 |
167 | return self.__cache
168 |
--------------------------------------------------------------------------------
/leaf/weixin/accesstoken/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | AccessToken 获取配置
3 | """
4 |
5 | # 默认最大的重试次数 - 大于零
6 | MaxRetries = 5
7 |
8 | # 每两次获取之间的时间间隔
9 | Pre = 30
10 | Gap = 7200 - Pre
11 |
12 | # 请求的地址
13 | Address = "https://api.weixin.qq.com/cgi-bin/token"
14 |
--------------------------------------------------------------------------------
/leaf/weixin/apis/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台API接口实现:
3 | template - 模板消息
4 | user - 用户相关
5 | """
6 |
7 | from .user import User as user
8 | from .template import TemplateMessage as template
9 |
--------------------------------------------------------------------------------
/leaf/weixin/apis/template.py:
--------------------------------------------------------------------------------
1 | """微信公众平台模板消息API实现"""
2 |
3 | from typing import Optional
4 |
5 | from ...core.tools import web
6 | from ..const import Request
7 |
8 |
9 | class TemplateMessage:
10 | """
11 | 微信公众平台模板消息API实现:
12 | setindustry - 设置模板所属行业
13 | """
14 |
15 | @staticmethod
16 | def setindustry(accesstoken: str, firstid: int, secondid: int) -> dict:
17 | """
18 | 设置模板消息库所属行业:
19 | firstid: 第一个行业 id
20 | secondid: 第二个行业 id
21 | """
22 | # 创建发送消息
23 | address = "https://api.weixin.qq.com/cgi-bin/template/api_set_industry"
24 | param = {Request.AccessToken: accesstoken}
25 | data = {
26 | Request.Template.IndustryFirst: firstid,
27 | Request.Template.IndustrySecond: secondid
28 | }
29 | request = web.JSONcreater(data)
30 |
31 | # 发送消息
32 | response, _ = web.post(address, param, request)
33 | response = web.JSONparser(response)
34 | return response
35 |
36 | @staticmethod
37 | def getindustry(accesstoken: str) -> dict:
38 | """
39 | 获取当前设置的行业信息
40 | """
41 | # 创建发送消息
42 | address = "https://api.weixin.qq.com/cgi-bin/template/get_industry"
43 | param = {Request.AccessToken: accesstoken}
44 |
45 | # 发送消息
46 | response, _ = web.get(address, param)
47 | response = web.JSONparser(response)
48 | return response
49 |
50 | @staticmethod
51 | def add(accesstoken: str, templateid: str) -> dict:
52 | """
53 | 向模板库中下载一个模板:
54 | templateid: 要增加的模板id
55 | """
56 | # 创建发送消息
57 | address = "https://api.weixin.qq.com/cgi-bin/template/api_add_template"
58 | param = {Request.AccessToken: accesstoken}
59 | request = {Request.Template.ID: templateid}
60 | request = web.JSONcreater(request)
61 |
62 | # 发送请求
63 | response, _ = web.post(address, param, request)
64 | response = web.JSONparser(response)
65 | return response
66 |
67 | @staticmethod
68 | def get(accesstoken: str) -> dict:
69 | """
70 | 获取本地模板库中所有的模板
71 | """
72 | # 创建发送的消息
73 | address = "https://api.weixin.qq.com/cgi-bin/template/get_all_private_template"
74 | param = {Request.AccessToken: accesstoken}
75 |
76 | # 发送请求
77 | response, _ = web.get(address, param)
78 | response = web.JSONparser(response)
79 | return response
80 |
81 | @staticmethod
82 | def delete(accesstoken: str, templateid: str) -> dict:
83 | """
84 | 删除库中已经下载好的某个模板:
85 | templateid: 要删除的模板id
86 | """
87 | # 创建发送消息
88 | address = "https://api.weixin.qq.com/cgi-bin/template/del_private_template"
89 | param = {Request.AccessToken: accesstoken}
90 | request = {Request.Template.ID: templateid}
91 |
92 | # 发送请求
93 | response, _ = web.post(address, param, request)
94 | response = web.JSONparser(response)
95 | return response
96 |
97 | @staticmethod
98 | def send(accesstoken: str, touser: str, templateid: str,
99 | data: dict, url: Optional[str] = None,
100 | appid: Optional[str] = None,
101 | pagepath: Optional[str] = None) -> dict:
102 | """
103 | 请求发送一个模板消息:
104 | touser: 要发送用户的 openid
105 | template_id: 要发送的模板 id
106 | data: 要发送的模板数据
107 | url: 跳转的网页链接
108 | appid: 要跳转的小程序 appid
109 | miniprogram: 要跳转的小程序数据
110 | pagepath: 要跳转的小程序路径
111 | *注意:
112 | 后面两个参数用来指定小程序跳转的相关参数,
113 | 当不指定时不进行跳转, 要提供时需要一起提供
114 | *data数据格式:
115 | {
116 | "格式化参数": ("数据", "颜色代码"),
117 | "first": ("您已经订阅成功", "#CCCAAA"),
118 | ...
119 | }
120 | """
121 | # 准备发送数据
122 | address = "https://api.weixin.qq.com/cgi-bin/message/template/send"
123 | param = {Request.AccessToken: accesstoken}
124 | sendata = dict()
125 |
126 | # 遍历生成数据
127 | for key, parameter in data.items():
128 | item, color = parameter
129 | sendata[key] = {
130 | Request.Template.Data.Value: item,
131 | Request.Template.Data.Color: color
132 | }
133 |
134 | request = {
135 | Request.Template.Data.Key: sendata,
136 | Request.Template.ToUser: touser,
137 | Request.Template.ID: templateid,
138 | }
139 |
140 | # 添加链接跳转
141 | if not url is None:
142 | request[Request.Template.JumpTo] = url
143 |
144 | # 添加小程序跳转信息
145 | if appid and pagepath:
146 | miniprogram = {
147 | Request.MiniProgram.AppID: appid,
148 | Request.MiniProgram.PagePath: pagepath
149 | }
150 | request[Request.Template.MiniProgram] = miniprogram
151 |
152 | request = web.JSONcreater(request)
153 |
154 | # 发送数据
155 | response, _ = web.post(address, param, request)
156 | response = web.JSONparser(response)
157 | return response
158 |
--------------------------------------------------------------------------------
/leaf/weixin/apis/user.py:
--------------------------------------------------------------------------------
1 | """微信公众平台用户相关API支持"""
2 |
3 | from typing import Iterable, Optional
4 |
5 | from ...core.tools import web
6 | from .. import settings
7 | from ..const import Request
8 |
9 |
10 | class User:
11 | """
12 | 微信公众平台用户相关功能支持:
13 | remark - 给用户设置备注
14 | info - 获取单个用户信息
15 | patch - 批量获取用户信息
16 | """
17 | @staticmethod
18 | def remark(accesstoken: str, openid: str, name: str) -> dict:
19 | """
20 | 给用户设置新的备注名:
21 | openid: 用户的 openid
22 | name: 用户的新昵称
23 | """
24 | # 创建发送地址和发送数据
25 | address = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark"
26 | data = web.JSONcreater({
27 | Request.User.OpenID: openid,
28 | Request.User.Remark: name
29 | })
30 |
31 | # 发送请求
32 | response, _ = web.post(
33 | address, {Request.AccessToken: accesstoken}, data)
34 | response = web.JSONparser(response)
35 | return response
36 |
37 | @staticmethod
38 | def info(accesstoken: str, openid: str,
39 | language: Optional[str] = settings.User.Language) -> dict:
40 | """
41 | 获取单个用户信息:
42 | openid: 要获取信息的用户openid
43 | """
44 | # 创建发送数据
45 | address = "https://api.weixin.qq.com/cgi-bin/user/info"
46 | data = {
47 | Request.AccessToken: accesstoken,
48 | Request.User.OpenID: openid,
49 | Request.User.Language: language
50 | }
51 |
52 | # 发送数据
53 | response, _ = web.get(address, data)
54 | response = web.JSONparser(response)
55 | return response
56 |
57 | @staticmethod
58 | def patch(accesstoken: str, openids: Iterable,
59 | language: Optional[str] = settings.User.Language) -> dict:
60 | """
61 | 批量获取用户信息:
62 | openids: 批量获取信息的用户 openid 列表
63 | """
64 | # 创建发送数据
65 | address = "https://api.weixin.qq.com/cgi-bin/user/info/batchget"
66 |
67 | # 按照微信的官方格式转换信息
68 | userlist = list()
69 | data = {Request.User.UserList: userlist}
70 | for openid in openids:
71 | userlist.append({
72 | Request.User.OpenID: openid,
73 | Request.User.Language: language
74 | })
75 |
76 | # 发送数据
77 | request = web.JSONcreater(data)
78 | response, _ = web.post(
79 | address, {Request.AccessToken: accesstoken}, request)
80 | response = web.JSONparser(response)
81 | return response
82 |
83 | @staticmethod
84 | def userlist(accesstoken: str, starting: Optional[str] = None) -> dict:
85 | """
86 | 获取关注公众号的用户 OpenID 列表:
87 | starting: 获取的第一个用户 OpenID - 不填则为从第一个开始
88 | """
89 | # 创建发送数据
90 | address = "https://api.weixin.qq.com/cgi-bin/user/get"
91 | if starting:
92 | params = {
93 | Request.AccessToken: accesstoken,
94 | Request.User.StartingPosition: starting
95 | }
96 | else:
97 | params = {
98 | Request.AccessToken: accesstoken
99 | }
100 |
101 | # 发送数据
102 | response, _ = web.get(address, params)
103 | response = web.JSONparser(response)
104 | return response
105 |
--------------------------------------------------------------------------------
/leaf/weixin/encrypt.py:
--------------------------------------------------------------------------------
1 | """微信公众平台消息加解密支持"""
2 |
3 | from ..core.tools import encrypt
4 | from . import const
5 |
6 |
7 | class Encrypt:
8 | """
9 | 为微信公众平台封装的消息加解密类
10 | """
11 | def __repr__(self) -> str:
12 | """返回 repr 信息"""
13 | return ""
14 |
15 | def __init__(self, aeskey: str, appid: str, token: str):
16 | """
17 | 加解密类构造函数:
18 | aeskey: 公众平台提供的 AESEncodingKey
19 | appid: 用户的 AppID
20 | token: 用户的 Token
21 | *注意: 这里的 Token 和 AccessToken 不是一个东西
22 | """
23 | # 公众平台提供的 AESEncodingKey 需要加上 = 之后进行 base64encode 使用
24 | self.__aeskey = encrypt.base64decode(aeskey + '=')
25 |
26 | # 保存 appid 和 token
27 | self.__appid = appid
28 | self.__token = token
29 |
30 | def encrypt(self, clear: str) -> str:
31 | """
32 | 加密函数 - 返回加密过后的密文
33 | clear - 未加密的明文:
34 | 1. 生成 16 位的随机 ASCII 字符串作为补位 -> rand = 16 bytes
35 | 2. 计算网络字节补位 b'x00x00x00x??' -> pad = 4 bytes
36 | 3. 计算 rand + pad + clear.encode() + appid.encode() -> msg
37 | 4. 对 msg PKCS7 补码(32 位补码) -> aligned = 32 * n bytes
38 | 5. 对 aligned 使用 self.__key 进行 AES.CBC 模式加密 -> cipher
39 | 6. 对 cipher 再一次进行 base64 编码
40 |
41 | *注意: 这里的明文指的是未经加密的XML全部字符串
42 | """
43 | # 准备加密需要的信息
44 | randstr: bytes = encrypt.random(const.Encrypt.PadLength).encode()
45 | pad: bytes = encrypt.packer(clear)
46 | msg: bytes = randstr + pad + clear.encode() + self.__appid.encode()
47 | aligned: bytes = encrypt.PKCS7encode(msg)
48 |
49 | # 进行 AES 加密
50 | cipher: bytes = encrypt.AESencrypt(aligned, self.__aeskey)
51 | cipher: bytes = encrypt.base64encode(cipher)
52 |
53 | return cipher.decode()
54 |
55 | def decrypt(self, cipher: bytes) -> str:
56 | """
57 | 解密函数 - 返回解密后的明文
58 | 按照 encrypt 加密函数倒序来一遍
59 | """
60 | # 按照顺序解密
61 | decoded: bytes = encrypt.base64decode(cipher)
62 | clear: bytes = encrypt.AESdecrypt(decoded, self.__aeskey)
63 | clear: bytes = encrypt.PKCS7decode(clear)
64 |
65 | # 前 16 为为随机字符串 - 取 16 位之后的信息
66 | content: bytes = clear[16:]
67 |
68 | # 去除 4bytes 的网络字节补位
69 | content = encrypt.unpacker(content)
70 |
71 | # 去除后 18 位的 APPID
72 | clear = content[0: -18]
73 |
74 | return clear.decode()
75 |
76 | def signature(self, message: str, timestamp: str, nonce: str) -> str:
77 | """
78 | 根据密文计算消息体签名:
79 | 1. 对 token, timestamp, nonce, msg_encrypted 进行排序
80 | 2. 排序之后进行字符串拼接
81 | 3. 返回拼接过后字符串的 SHA1 值
82 | """
83 | calculation = [message, timestamp, nonce, self.__token]
84 | calculation.sort()
85 |
86 | combined: str = ''.join(calculation)
87 | siganture: str = encrypt.SHA1(combined)
88 |
89 | return siganture
90 |
--------------------------------------------------------------------------------
/leaf/weixin/errcodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "40001": "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口",
3 | "40002": "不合法的凭证类型",
4 | "40003": "不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID",
5 | "40004": "不合法的媒体文件类型",
6 | "40005": "不合法的文件类型",
7 | "40006": "不合法的文件大小",
8 | "40007": "不合法的媒体文件 id",
9 | "40008": "不合法的消息类型",
10 | "40009": "不合法的图片文件大小",
11 | "40010": "不合法的语音文件大小",
12 | "40011": "不合法的视频文件大小",
13 | "40012": "不合法的缩略图文件大小",
14 | "40013": "不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写",
15 | "40014": "不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口",
16 | "40015": "不合法的菜单类型",
17 | "40016": "不合法的按钮个数",
18 | "40017": "不合法的按钮类型",
19 | "40018": "不合法的按钮名字长度",
20 | "40019": "不合法的按钮 KEY 长度",
21 | "40020": "不合法的按钮 URL 长度",
22 | "40021": "不合法的菜单版本号",
23 | "40022": "不合法的子菜单级数",
24 | "40023": "不合法的子菜单按钮个数",
25 | "40024": "不合法的子菜单按钮类型",
26 | "40025": "不合法的子菜单按钮名字长度",
27 | "40026": "不合法的子菜单按钮 KEY 长度",
28 | "40027": "不合法的子菜单按钮 URL 长度",
29 | "40028": "不合法的自定义菜单使用用户",
30 | "40029": "无效的 oauth_code",
31 | "40030": "不合法的 refresh_token",
32 | "40031": "不合法的 openid 列表",
33 | "40032": "不合法的 openid 列表长度",
34 | "40033": "不合法的请求字符,不能包含 \\uxxxx 格式的字符",
35 | "40035": "不合法的参数",
36 | "40038": "不合法的请求格式",
37 | "40039": "不合法的 URL 长度",
38 | "40048": "无效的url",
39 | "40050": "不合法的分组 id",
40 | "40051": "分组名字不合法",
41 | "40060": "删除单篇图文时,指定的 article_idx 不合法",
42 | "40117": "分组名字不合法",
43 | "40118": "media_id 大小不合法",
44 | "40119": "button 类型错误",
45 | "40120": "子 button 类型错误",
46 | "40121": "不合法的 media_id 类型",
47 | "40125": "无效的appsecret",
48 | "40132": "微信号不合法",
49 | "40137": "不支持的图片格式",
50 | "40155": "请勿添加其他公众号的主页链接",
51 | "40163": "oauth_code已使用",
52 | "40164": "调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。(小程序及小游戏调用不要求IP地址在白名单内。)",
53 | "41001": "缺少 access_token 参数",
54 | "41002": "缺少 appid 参数",
55 | "41003": "缺少 refresh_token 参数",
56 | "41004": "缺少 secret 参数",
57 | "41005": "缺少多媒体文件数据",
58 | "41006": "缺少 media_id 参数",
59 | "41007": "缺少子菜单数据",
60 | "41008": "缺少 oauth code",
61 | "41009": "缺少 openid",
62 | "42001": "access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明",
63 | "42002": "refresh_token 超时",
64 | "42003": "oauth_code 超时",
65 | "42007": "用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权",
66 | "43001": "需要 GET 请求",
67 | "43002": "需要 POST 请求",
68 | "43003": "需要 HTTPS 请求",
69 | "43004": "需要接收者关注",
70 | "43005": "需要好友关系",
71 | "43019": "需要将接收者从黑名单中移除",
72 | "44001": "多媒体文件为空",
73 | "44002": "POST 的数据包为空",
74 | "44003": "图文消息内容为空",
75 | "44004": "文本消息内容为空",
76 | "45001": "多媒体文件大小超过限制",
77 | "45002": "消息内容超过限制",
78 | "45003": "标题字段超过限制",
79 | "45004": "描述字段超过限制",
80 | "45005": "链接字段超过限制",
81 | "45006": "图片链接字段超过限制",
82 | "45007": "语音播放时间超过限制",
83 | "45008": "图文消息超过限制",
84 | "45009": "接口调用超过限制",
85 | "45010": "创建菜单个数超过限制",
86 | "45011": "API 调用太频繁,请稍候再试",
87 | "45015": "回复时间超过限制",
88 | "45016": "系统分组,不允许修改",
89 | "45017": "分组名字过长",
90 | "45018": "分组数量超过上限",
91 | "45047": "客服接口下行条数超过上限",
92 | "45064": "创建菜单包含未关联的小程序",
93 | "45065": "相同 clientmsgid 已存在群发记录,返回数据中带有已存在的群发任务的 msgid",
94 | "45066": "相同 clientmsgid 重试速度过快,请间隔1分钟重试",
95 | "45067": "clientmsgid 长度超过限制",
96 | "46001": "不存在媒体数据",
97 | "46002": "不存在的菜单版本",
98 | "46003": "不存在的菜单数据",
99 | "46004": "不存在的用户",
100 | "47001": "解析 JSON/XML 内容错误",
101 | "48001": "api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限",
102 | "48002": "粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” )",
103 | "48004": "api 接口被封禁,请登录 mp.weixin.qq.com 查看详情",
104 | "48005": "api 禁止删除被自动回复和自定义菜单引用的素材",
105 | "48006": "api 禁止清零调用次数,因为清零次数达到上限",
106 | "48008": "没有该类型消息的发送权限",
107 | "50001": "用户未授权该 api",
108 | "50002": "用户受限,可能是违规后接口被封禁",
109 | "50005": "用户未关注公众号",
110 | "61451": "参数错误 (invalid parameter)",
111 | "61452": "无效客服账号 (invalid kf_account)",
112 | "61453": "客服帐号已存在 (kf_account exsited)",
113 | "61454": "客服帐号名长度超过限制 (仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号)(invalid kf_acount length)",
114 | "61455": "客服帐号名包含非法字符 (仅允许英文 + 数字)(illegal character in kf_account)",
115 | "61456": "客服帐号个数超过限制 (10 个客服账号)(kf_account count exceeded)",
116 | "61457": "无效头像文件类型 (invalid file type)",
117 | "61450": "系统错误 (system error)",
118 | "61500": "日期格式错误",
119 | "63001": "部分参数为空",
120 | "63002": "无效的签名",
121 | "65301": "不存在此 menuid 对应的个性化菜单",
122 | "65302": "没有相应的用户",
123 | "65303": "没有默认菜单,不能创建个性化菜单",
124 | "65304": "MatchRule 信息为空",
125 | "65305": "个性化菜单数量受限",
126 | "65306": "不支持个性化菜单的帐号",
127 | "65307": "个性化菜单信息为空",
128 | "65308": "包含没有响应类型的 button",
129 | "65309": "个性化菜单开关处于关闭状态",
130 | "65310": "填写了省份或城市信息,国家信息不能为空",
131 | "65311": "填写了城市信息,省份信息不能为空",
132 | "65312": "不合法的国家信息",
133 | "65313": "不合法的省份信息",
134 | "65314": "不合法的城市信息",
135 | "65316": "该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接)",
136 | "65317": "不合法的 URL",
137 | "87009": "无效的签名",
138 | "89503": "此IP调用需要管理员确认,请联系管理员",
139 | "89501": "此IP正在等待管理员确认,请联系管理员",
140 | "89506": "24小时内该IP被管理员拒绝调用两次,24小时内不可再使用该IP调用",
141 | "89507": "1小时内该IP被管理员拒绝调用一次,1小时内不可再使用该IP调用"
142 | }
--------------------------------------------------------------------------------
/leaf/weixin/error.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台错误消息相关
3 | """
4 |
5 | from ..core import error
6 |
7 |
8 | class SignatureError(error.Error):
9 | """消息体签名错误"""
10 | code = 12001
11 | description = "对消息体的签名验证出现错误"
12 |
13 |
14 | class EncryptError(error.Error):
15 | """消息体加密错误"""
16 | code = 12002
17 | description = "在消息体加密过程中出现错误"
18 |
19 |
20 | class DecryptError(error.Error):
21 | """消息体解密错误"""
22 | code = 12003
23 | description = "在消息体解密过程中发生错误"
24 |
25 |
26 | class InvalidMessage(error.Error):
27 | """消息体非法"""
28 | code = 12004
29 | description = "消息体不正确(键缺少/数据类型非法)"
30 |
--------------------------------------------------------------------------------
/leaf/weixin/reply/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台消息回复实现工具
3 | maker - 消息回复制作器
4 | message - 消息类型回复
5 | event - 事件类型回复
6 | url - url 参数验证
7 | """
8 |
9 | from .event import Event
10 | from .message import Message
11 | from .url import URLParamater as Url
12 | from .maker import MakeReply as Maker
13 |
--------------------------------------------------------------------------------
/leaf/weixin/reply/event.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台事件类型处理
3 | """
4 |
5 | import logging
6 | from collections import deque
7 | from typing import NoReturn
8 |
9 | from . import url
10 |
11 | from .. import const
12 | from .. import settings
13 | from ..encrypt import Encrypt
14 |
15 | from ...core import events
16 | from ...core import wrapper
17 | from ...core import modules
18 |
19 | logger = logging.getLogger("leaf.weixin.reply")
20 | subscribe = events.Event("leaf.weixin.events.subscribe",
21 | ((str, int), {}), "用户关注")
22 | unsubscribe = events.Event("leaf.weixin.events.unsubscribe",
23 | ((str, int), {}), "用户取消关注")
24 | scan = events.Event("leaf.weixin.events.scan",
25 | ((str, int), {"key": str, "ticket": str}), "用户扫描二维码")
26 | location = events.Event("leaf.weixin.events.location",
27 | ((str, int), {"latitude": str,
28 | "longitude": str,
29 | "precision": str}),
30 | "用户更新地理位置")
31 | click = events.Event("leaf.weixin.events.menu.click",
32 | ((str, int), {"key": str}), "用户调取菜单事件")
33 | view = events.Event("leaf.weixin.events.menu.view",
34 | ((str, int), {"key": str}), "用户点击菜单进行跳转")
35 | pushed = events.Event("leaf.weixin.events.push.success",
36 | ((str, str), {"id": str}), "模板消息推送完成(请自行判断是否成功)")
37 |
38 |
39 | class Event:
40 | """
41 | 事件类型处理类:
42 | handle - 价格事件对应的名称传入, 调用所有绑定事件的函数
43 | """
44 |
45 | def __init__(self, encryptor: Encrypt):
46 | """初始化事件处理函数"""
47 | # 注册事件
48 | manager: events.Manager = modules.events
49 | manager.add(subscribe)
50 | manager.add(unsubscribe)
51 | manager.add(scan)
52 | manager.add(location)
53 | manager.add(click)
54 | manager.add(view)
55 | manager.add(pushed)
56 |
57 | # 注册实例
58 | self.__encryptor = encryptor # 加解密实例
59 | self.__exclusion = deque(
60 | maxlen=settings.Message.ExclusionLength) # 消息排重队列
61 |
62 | @wrapper.timelimit(settings.Message.TimeOut)
63 | def handle(self, paramaters: list, request: str) -> NoReturn:
64 | """
65 | 将事件数据包传入调用事件:
66 | paramaters: URL参数列表
67 | request: POST数据字典
68 | 判断加密与进行消息排重
69 | 根据事件类型提取信息
70 | 调用指定的事件
71 | """
72 | _encrypted, message = url.verify(self.__encryptor, paramaters, request)
73 |
74 | # 获取用户openid, 事件时间, 事件类型
75 | user = message.get(const.Message.From)
76 | created = message.get(const.Message.CreateTime)
77 | event_type = message.get(const.Event.Type)
78 |
79 | # 进行消息排重
80 | msgid = user + created
81 | if msgid in self.__exclusion:
82 | return
83 | self.__exclusion.append(msgid)
84 |
85 | # 获取要拉取的事件与参数
86 | try:
87 | event_name = const.Event.Events.get(event_type)
88 | event: events.Event = modules.events.event(event_name)
89 | except events.EventNotFound as _error:
90 | logger.error(_error)
91 | return
92 |
93 | paras = dict()
94 | for key, value in const.Event.Types.get(event_type):
95 | paras[value] = message.get(key, '')
96 |
97 | # 拉动事件
98 | event.notify(user, int(created), **paras)
99 |
--------------------------------------------------------------------------------
/leaf/weixin/reply/maker.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台消息回复制作器
3 | """
4 |
5 | from typing import Optional, List
6 | from .. import const
7 |
8 |
9 | class MakeReply:
10 | """
11 | 制作消息回复包
12 | 制作出的消息返回值格式为:
13 | msgtype: str - 消息类型
14 | msg: dict - 返回的消息
15 | 函数调用示例:
16 | MakeReply.text("HELLO WORLD!") ->
17 | ("text", {"Content": "HELLO WORLD!"})
18 | """
19 | @staticmethod
20 | def text(content: str) -> dict:
21 | """
22 | 回复文本消息:
23 | content: 回复内容
24 | """
25 | return {
26 | const.Message.Type: const.Reply.Types.Text,
27 | const.Reply.Text.Key: content
28 | }
29 |
30 | @staticmethod
31 | def picture(mediaid: str) -> dict:
32 | """
33 | 回复图片消息:
34 | mediaid: 图片mediaid
35 | """
36 | return {
37 | const.Message.Type: const.Reply.Types.Image,
38 | const.Reply.Image.Key: {
39 | const.Reply.Image.MediaId: mediaid
40 | }
41 | }
42 |
43 | @staticmethod
44 | def voice(mediaid: str) -> dict:
45 | """
46 | 回复语音消息:
47 | mediaid: 语言 mediaid
48 | """
49 | return {
50 | const.Message.Type: const.Reply.Types.Voice,
51 | const.Reply.Types.Voice: {
52 | const.Reply.Voice.MediaId: mediaid
53 | }
54 | }
55 |
56 | @staticmethod
57 | def video(mediaid: str, title: Optional[str] = None,
58 | description: Optional[str] = None) -> dict:
59 | """
60 | 回复视频消息:
61 | mediaid: 视频 mediaid
62 | title: 标题 - 可选
63 | description: 描述 - 可选
64 | """
65 | content = {const.Reply.Video.MediaId: mediaid}
66 |
67 | # 如果有附加的键则添加消息
68 | if not title is None:
69 | content[const.Reply.Video.Title] = title
70 | if not description is None:
71 | content[const.Reply.Video.Description] = description
72 |
73 | return {
74 | const.Message.Type: const.Reply.Types.Video,
75 | const.Reply.Types.Video: content
76 | }
77 |
78 | @staticmethod
79 | def music(thumb: str, title: Optional[str] = None,
80 | description: Optional[str] = None,
81 | url: Optional[str] = None,
82 | HQ: Optional[str] = None) -> dict:
83 | """
84 | 回复音乐消息:
85 | thumb: 缩略图mediaid
86 | title: 标题 - 可选
87 | description: 描述 - 可选
88 | url: 地址 - 可选
89 | HQ: 高清地址 - 可选
90 | """
91 | content = {const.Reply.Music.Thumb: thumb}
92 |
93 | # 添加可选信息
94 | if not title is None:
95 | content[const.Reply.Music.Title] = title
96 | if not description is None:
97 | content[const.Reply.Music.Description] = description
98 | if not url is None:
99 | content[const.Reply.Music.URL] = url
100 | if not HQ is None:
101 | content[const.Reply.Music.HQURL] = HQ
102 |
103 | return {
104 | const.Message.Type: const.Reply.Types.Music,
105 | const.Reply.Types.Music: content
106 | }
107 |
108 | @staticmethod
109 | def article(title: str, description: str,
110 | image: str, redirect: str) -> dict:
111 | """
112 | 生成一条文章回复:
113 | title: 文章标题
114 | description: 文章描述
115 | image: 图片地址
116 | redirect: 跳转地址
117 | *请勿单独调用该函数作为回复
118 | *请将结果传入 articles 函数包装
119 | """
120 | return {
121 | const.Reply.Article.Item.Title: title,
122 | const.Reply.Article.Item.Picture: image,
123 | const.Reply.Article.Item.URL: redirect,
124 | const.Reply.Article.Item.Descritpion: description
125 | }
126 |
127 | @staticmethod
128 | def articles(articles: List[dict]) -> dict:
129 | """
130 | 回复图文消息:
131 | articles: 使用 article 函数生成的字典列表
132 | """
133 | data = [{const.Reply.Article.Item.Key: item} for item in articles]
134 | return {
135 | const.Reply.Article.Count: len(articles),
136 | const.Reply.Types.Article: data
137 | }
138 |
--------------------------------------------------------------------------------
/leaf/weixin/reply/message.py:
--------------------------------------------------------------------------------
1 | """
2 | 微信公众平台消息接受与被动回复
3 |
4 | """
5 |
6 | from collections import deque
7 | from typing import Callable, Tuple, Any
8 |
9 | from . import url
10 |
11 | from .. import const
12 | from .. import settings
13 | from ..encrypt import Encrypt
14 |
15 | from ...core import wrapper
16 | from ...core.tools import web
17 | from ...core.tools import time
18 | from ...core.tools import encrypt
19 |
20 |
21 | class Message:
22 | """
23 | 消息回复管理器:
24 | register - 注册消息回复函数
25 | reply - 对给定的包进行回复(包含加/解密流程)
26 | """
27 |
28 | def __init__(self, encryptor: Encrypt):
29 | """
30 | 初始化一个消息回复管理器
31 | 以消息类型到用于处理消息函数的映射
32 | """
33 | self.__encryptor = encryptor # 加密处理函数
34 | self.__registry = {} # 注册函数处理: str - callable
35 | self.__exclusion = deque(
36 | maxlen=settings.Message.ExclusionLength) # 消息排重队列
37 |
38 | def register(self, typing: str) -> Callable:
39 | """
40 | 注册一种消息的回复函数
41 | 在微信发来的消息字段 MsgType 中定义的
42 | 字段参数不区分大小写, 调用示例:
43 | @weixin.reply.register("text")
44 | def text_reply(**kwargs):
45 | # 返回一样字符串
46 | content = kwargs.get("content")
47 | return content
48 | """
49 | def _wrapper(function: Callable[[Any], Tuple[str, dict]]) -> Callable:
50 | self.__registry.update({typing: function})
51 |
52 | return _wrapper
53 |
54 | @wrapper.timelimit(settings.Message.TimeOut)
55 | def reply(self, paramaters: list, request: str) -> str:
56 | """
57 | 返回根据消息调用函数返回回复:
58 | paramaters: URL参数列表
59 | request: POST数据字典
60 | 判断加密与进行消息排重
61 | 根据消息类型提取出所有需要的信息
62 | 将信息传入取到的函数进行回复
63 | """
64 | encrypted, message = url.verify(self.__encryptor, paramaters, request)
65 |
66 | # 获取五元素
67 | touser = message.get(const.Message.To)
68 | fromuser = message.get(const.Message.From)
69 | _created = message.get(const.Message.CreateTime)
70 | msgid = message.get(const.Message.Id)
71 | msgtype = message.get(const.Message.Type)
72 |
73 | # 进行消息排重
74 | if msgid in self.__exclusion:
75 | return settings.Message.EmptyReply
76 | self.__exclusion.append(msgid)
77 |
78 | # 获取需要从包中提取的信息
79 | needed_keys = const.Message.Types.get(msgtype)
80 | if needed_keys is None:
81 | return settings.Message.EmptyReply
82 |
83 | useful = dict()
84 | for key, value in needed_keys.items():
85 | useful[value] = message.get(key, '')
86 |
87 | # 获取用于处理的函数
88 | handler = self.__registry.get(msgtype)
89 | if handler is None:
90 | return settings.Message.EmptyReply
91 |
92 | # 获取处理消息
93 | preduced: dict = handler(**useful)
94 |
95 | reply = {
96 | const.Message.From: touser,
97 | const.Message.To: fromuser,
98 | const.Message.CreateTime: time.now()
99 | }
100 |
101 | reply.update(preduced)
102 | reply = web.XMLcreater({const.Message.Root: reply}, encoding=False)
103 |
104 | # 设置回包加密
105 | if not encrypted:
106 | return reply
107 |
108 | nonce = encrypt.random(16)
109 | timestamp = time.nowstr()
110 | cipher = self.__encryptor.encrypt(reply)
111 | signature = self.__encryptor.signature(
112 | cipher, timestamp, nonce)
113 |
114 | reply = {
115 | const.Encrypt.Message.Content: cipher,
116 | const.Encrypt.Message.Signature: signature,
117 | const.Encrypt.Message.TimeStamp: timestamp,
118 | const.Encrypt.Message.Nonce: nonce
119 | }
120 | return web.XMLcreater({const.Message.Root: reply}, encoding=False)
121 |
--------------------------------------------------------------------------------
/leaf/weixin/reply/url.py:
--------------------------------------------------------------------------------
1 | """URL 参数验证器"""
2 |
3 | from typing import Tuple
4 |
5 | from .. import const
6 | from .. import error
7 |
8 | from ..encrypt import Encrypt
9 | from ...core.tools import web
10 |
11 |
12 | class URLParamater:
13 | """URL 参数处理器"""
14 |
15 | @staticmethod
16 | def nonce(paramaters: dict) -> str:
17 | """返回 URL 中的 Nonce"""
18 | return paramaters.get(const.Encrypt.URL.Key.Nonce, '')
19 |
20 | @staticmethod
21 | def timestamp(paramaters: dict) -> str:
22 | """返回时间戳参数"""
23 | return paramaters.get(const.Encrypt.URL.Key.TimeStamp, '')
24 |
25 | @staticmethod
26 | def siganture(paramaters: dict) -> str:
27 | """返回消息体签名"""
28 | return paramaters.get(const.Encrypt.URL.Key.Signature, '')
29 |
30 | @staticmethod
31 | def encrypted(paramaters: dict) -> bool:
32 | """判断数据包是否是加密的"""
33 | encrypt_type = paramaters.get(const.Encrypt.URL.Key.Encrypted)
34 | if encrypt_type == const.Encrypt.URL.Value.Encrypted:
35 | return True
36 | return False
37 |
38 |
39 | def verify(encryptor: Encrypt, paramaters: list, request: str) -> Tuple[bool, dict]:
40 | """
41 | 验证加密是否正确
42 | 尝试解密后将xml转为字典输出
43 | 返回是否加密的 bool 值与解包后的字典
44 | """
45 | request = web.XMLparser(request)
46 | request = request.get(const.Message.Root)
47 |
48 | # 获取URL中的参数
49 | encrypted = URLParamater.encrypted(paramaters)
50 | signature = URLParamater.siganture(paramaters)
51 | nonce = URLParamater.nonce(paramaters)
52 | timestamp = URLParamater.timestamp(paramaters)
53 |
54 | # 判断是否加密
55 | if not encrypted:
56 | return False, request
57 |
58 | # 验证加密是否正确
59 | encrypted_msg: str = request.get(const.Encrypt.Message.Content)
60 | expectation = encryptor.signature(
61 | encrypted_msg, timestamp, nonce)
62 | if expectation != signature:
63 | raise error.SignatureError(expectation + ' - ' + signature)
64 |
65 | # 解密数据包
66 | message = dict()
67 | try:
68 | message = encryptor.decrypt(encrypted_msg.encode())
69 | message = web.XMLparser(message)
70 | message = message.get(const.Message.Root)
71 | except ValueError as _error:
72 | raise error.DecryptError(_error)
73 |
74 | return True, message
75 |
--------------------------------------------------------------------------------
/leaf/weixin/settings.py:
--------------------------------------------------------------------------------
1 | """微信公众平台设置"""
2 |
3 | import os
4 |
5 | Interface = "callback" # 微信公众平台的回调地址
6 | ErrcodesFile = os.path.dirname(os.path.realpath(__file__)) + "/" + "errcodes.json" # 错误代码存储文件
7 |
8 | class User:
9 | """用户信息相关设置"""
10 | Language = "zh_CN" # 获取用户信息的语言版本
11 |
12 | class Message:
13 | """消息相关设置"""
14 | EmptyReply = "success" # 回复空消息
15 | TimeOut = 4.1 # 每个回复消息的超时时间 - 推荐小于 5 秒
16 | ExclusionLength = 128 # 消息排重队列长度
17 |
--------------------------------------------------------------------------------
/linting.py:
--------------------------------------------------------------------------------
1 | """Linting and return score as system code"""
2 |
3 | from os import system
4 | from pylint.lint import Run
5 |
6 | results = Run(["leaf"], do_exit=False)
7 | score = round(results.linter.stats["global_note"], 2)
8 | system('echo "::set-output name=score::' + str(score) + '"')
9 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==1.1.1
2 | pymongo==3.9.0
3 | mongoengine==0.18.2
4 | Werkzeug==0.16.0
5 | pycryptodome==3.9.4
6 | cacheout==0.11.2
7 | Pillow>=7.1.0
8 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | """框架运行示例文件"""
2 |
3 | import logging
4 | import flask
5 |
6 | import leaf
7 | import config
8 |
9 | init = leaf.Init()
10 | init.kernel(config.basic)
11 | init.logging(config.logging)
12 | init.server(config.devlopment)
13 | init.database(config.database)
14 |
15 | # 以下模块请根据需要启用/禁用初始化
16 | init.weixin(config.weixin) # 微信公众平台支持模块
17 | init.wxpay(config.wxpay) # 微信支付支持模块
18 | init.plugins(config.plugins) # 插件模块
19 |
20 | # 获取服务模块
21 | server: flask.Flask = leaf.modules.server
22 |
23 | # 以下模块的获取根据您上面的启用情况以及需求进行
24 | plugins: leaf.plugins.Manager = leaf.modules.plugins # 插件系统模块实例
25 | logger: logging.Logger = leaf.modules.logging.logger # 日志系统模块实例
26 | events: leaf.core.events.Manager = leaf.modules.events # 事件系统模块实例
27 | wxpay: leaf.core.algorithm.AttrDict = leaf.payments.wxpay # 微信支付模块实例
28 | weixin: leaf.core.algorithm.AttrDict = leaf.modules.weixin # 微信公众平台模块实例
29 | schedules: leaf.core.schedule.Manager = leaf.modules.schedules # 任务调度模块实例
30 |
31 | # 如果需要启用 Flask 自带的 Web Server 进行调试
32 | # 请取消下面一行的注释并给定 Flask 运行参数即可
33 | # server.run(host="127.0.0.1", port=8080)
34 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """打包发布文件"""
2 |
3 | import setuptools
4 |
5 | VERSION = "1.0.9.1.dev8"
6 |
7 | # 读取项目说明
8 | with open("README.md", "r", encoding="utf-8") as handler:
9 | long_description = handler.read()
10 |
11 | # 读取依赖
12 | with open("requirements.txt", "r") as handler:
13 | string = handler.read()
14 | pvs = string.split('\n')
15 | packages = [pv.split("==")[0] for pv in pvs if pv]
16 | # packages = [pv for pv in pvs if pv]
17 |
18 | setuptools.setup(
19 | name="wxleaf",
20 | version=VERSION,
21 | author="Gui Qiqi",
22 | license="Apache Software License",
23 | install_requires=packages,
24 | author_email="guiqiqi187@gmail.com",
25 | description="一个开发友好、功能完备的开源微信商城框架",
26 | long_description=long_description,
27 | long_description_content_type="text/markdown",
28 | url="https://github.com/guiqiqi/leaf",
29 | packages=setuptools.find_packages(),
30 | package_data={
31 | "leaf.weixin": ["errcodes.json"]
32 | },
33 | classifiers=[
34 | "Programming Language :: Python :: 3.6",
35 | "Programming Language :: Python :: 3.7",
36 | "Programming Language :: Python :: 3.8",
37 | "Programming Language :: Python :: 3.9",
38 | "Development Status :: 1 - Planning",
39 | "Framework :: Flask",
40 | "Topic :: Software Development :: Libraries :: Application Frameworks",
41 | "License :: OSI Approved :: Apache Software License",
42 | "Natural Language :: Chinese (Simplified)",
43 | "Operating System :: OS Independent"
44 | ],
45 | python_requires=">=3.6"
46 | )
47 |
--------------------------------------------------------------------------------