rogerxavier commited on
Commit
2fb449c
·
verified ·
1 Parent(s): 41541c7

Upload 23 files

Browse files
README.md CHANGED
@@ -1,12 +1,84 @@
1
  ---
2
- title: PayHook
3
- emoji: 👁
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.38.1
8
- app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: payHook - Payment Webhook Handler
3
+ emoji: 🐨
4
+ colorFrom: pink
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 4.36.1
8
+ app_file: main.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
+ # payHook - 支付回调处理服务
14
+
15
+ 这是一个用于处理支付回调的Web服务,支持爱发电(Afdian)等平台的支付通知处理。
16
+
17
+ ## 功能特性
18
+
19
+ - 支付回调处理
20
+ - 用户账户管理
21
+ - 余额更新
22
+ - 订阅时间管理
23
+ - 安全的API接口
24
+
25
+ ## 快速开始
26
+
27
+ ### 1. 环境配置
28
+
29
+ 在 Hugging Face Spaces 中:
30
+ 1. 进入 Space 设置页面
31
+ 2. 点击 "Settings" > "Repository secrets"
32
+ 3. 添加必要的环境变量(详见 [SECURITY.md](SECURITY.md))
33
+
34
+ ### 2. 本地开发
35
+
36
+ ```bash
37
+ # 克隆项目
38
+ git clone <your-repo-url>
39
+ cd payHook
40
+
41
+ # 安装依赖
42
+ pip install -r requirements.txt
43
+
44
+ # 配置环境变量
45
+ cp env.example .env
46
+ # 编辑 .env 文件填入实际配置
47
+
48
+ # 运行服务
49
+ python main.py
50
+ ```
51
+
52
+ ## 安全说明
53
+
54
+ 本项目仅用于学习和演示目的,生产环境请务必:
55
+ - 使用环境变量管理敏感信息
56
+ - 添加适当的身份验证
57
+ - 实施速率限制
58
+ - 使用HTTPS
59
+
60
+ ## 配置
61
+
62
+ 请通过环境变量配置以下敏感信息:
63
+ - `ALIYUN_ACCESS_KEY_ID` - 阿里云访问密钥ID
64
+ - `ALIYUN_ACCESS_KEY_SECRET` - 阿里云访问密钥Secret
65
+ - `ALIYUN_ENDPOINT` - 阿里云TableStore端点
66
+ - `ALIYUN_INSTANCE_NAME` - 阿里云TableStore实例名
67
+ - `MYSQL_HOST` - MySQL主机地址
68
+ - `MYSQL_USER` - MySQL用户名
69
+ - `MYSQL_PASSWORD` - MySQL密码
70
+ - `MYSQL_DATABASE` - MySQL数据库名
71
+
72
+ ## API 文档
73
+
74
+ 启动服务后,访问 `http://localhost:7860/docs` 查看完整的API文档。
75
+
76
+ ## 安全更新
77
+
78
+ - ✅ 移除硬编码的敏感信息
79
+ - ✅ 添加环境变量支持
80
+ - ✅ 增强错误处理和日志记录
81
+ - ✅ 添加参数验证
82
+ - ✅ 完善文档说明
83
+
84
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
SECURITY.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 安全配置指南
2
+
3
+ ## 概述
4
+
5
+ 本项目是一个支付回调处理服务,处理敏感信息时需要特别注意安全性。
6
+
7
+ ## 环境变量配置
8
+
9
+ ### 在 Hugging Face Spaces 中配置
10
+
11
+ 1. 进入你的 Space 设置页面
12
+ 2. 点击 "Settings" > "Repository secrets"
13
+ 3. 添加以下环境变量:
14
+
15
+ ```
16
+ ALIYUN_ACCESS_KEY_ID=你的阿里云访问密钥ID
17
+ ALIYUN_ACCESS_KEY_SECRET=你的阿里云访问密钥Secret
18
+ ALIYUN_ENDPOINT=你的阿里云TableStore端点
19
+ ALIYUN_INSTANCE_NAME=你的阿里云TableStore实例名
20
+ MYSQL_HOST=你的MySQL主机地址
21
+ MYSQL_USER=你的MySQL用户名
22
+ MYSQL_PASSWORD=你的MySQL密码
23
+ MYSQL_DATABASE=你的MySQL数据库名
24
+ ```
25
+
26
+ ### 本地开发配置
27
+
28
+ 1. 复制 `env.example` 为 `.env`
29
+ 2. 填入实际的配置值
30
+ 3. 确保 `.env` 文件已添加到 `.gitignore`
31
+
32
+ ## 安全最佳实践
33
+
34
+ 1. **永远不要在代码中硬编码敏感信息**
35
+ 2. **定期轮换访问密钥**
36
+ 3. **使用最小权限原则**
37
+ 4. **启用日志记录监控异常活动**
38
+ 5. **定期更新依赖包**
39
+ 6. **使用HTTPS进行生产环境通信**
40
+
41
+ ## 错误处理
42
+
43
+ 项目已添加完善的错误处理和日志记录,包括:
44
+ - 参数验证
45
+ - 异常捕获和记录
46
+ - 用户友好的错误消息
47
+ - 敏感信息过滤
48
+
49
+ ## 监控和日志
50
+
51
+ 所有关键操作都会记录日志,包括:
52
+ - 用户登录/注册
53
+ - 支付处理
54
+ - 数据库操作
55
+ - 错误和异常
56
+
57
+ ## 合规性
58
+
59
+ 本项目仅用于学习和演示目的。在生产环境中使用时,请确保:
60
+ - 符合相关数据保护法规
61
+ - 实施适当的数据加密
62
+ - 建立数据备份和恢复机制
63
+ - 定期进行安全审计
env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 阿里云 TableStore 配置
2
+ ALIYUN_ACCESS_KEY_ID=your_access_key_id
3
+ ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
4
+ ALIYUN_ENDPOINT=your_endpoint
5
+ ALIYUN_INSTANCE_NAME=your_instance_name
6
+
7
+ # MySQL 数据库配置
8
+ MYSQL_HOST=your_mysql_host
9
+ MYSQL_USER=your_mysql_user
10
+ MYSQL_PASSWORD=your_mysql_password
11
+ MYSQL_DATABASE=your_database_name
main.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ import os
3
+ import sys
4
+
5
+ def check_environment():
6
+ """检查必要的环境变量"""
7
+ required_vars = [
8
+ 'ALIYUN_ACCESS_KEY_ID',
9
+ 'ALIYUN_ACCESS_KEY_SECRET',
10
+ 'ALIYUN_ENDPOINT',
11
+ 'ALIYUN_INSTANCE_NAME'
12
+ ]
13
+
14
+ missing_vars = []
15
+ for var in required_vars:
16
+ if not os.getenv(var):
17
+ missing_vars.append(var)
18
+
19
+ if missing_vars:
20
+ print("警告: 缺少以下环境变量:")
21
+ for var in missing_vars:
22
+ print(f" - {var}")
23
+ print("\n请设置这些环境变量以确保服务正常运行。")
24
+ print("在Hugging Face Spaces中,请在Settings > Repository secrets中添加这些变量。")
25
+
26
+ if __name__ == "__main__":
27
+ check_environment()
28
+ uvicorn.run("server.api:app", host="0.0.0.0", port=7860, reload=True)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ requests==2.31.0
2
+ aiohttp==3.7.3
3
+ fastapi==0.112.0
4
+ urllib3==2.0.7
5
+ tablestore==6.0.1
6
+ pydantic==2.5.0
7
+ uvicorn==0.27.0
8
+ mysql-connector-python==8.2.0
9
+ python-dotenv==1.0.0
server/__pycache__/api.cpython-38.pyc ADDED
Binary file (860 Bytes). View file
 
server/__pycache__/dao_pool.cpython-38.pyc ADDED
Binary file (1.73 kB). View file
 
server/__pycache__/getPoints.cpython-38.pyc ADDED
Binary file (982 Bytes). View file
 
server/__pycache__/getPoolStatus.cpython-38.pyc ADDED
Binary file (474 Bytes). View file
 
server/__pycache__/invite_code_api.cpython-38.pyc ADDED
Binary file (1.26 kB). View file
 
server/__pycache__/utils.cpython-38.pyc ADDED
Binary file (919 Bytes). View file
 
server/__pycache__/verification.cpython-38.pyc ADDED
Binary file (2.33 kB). View file
 
server/account_manager.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import logging
3
+ from fastapi import HTTPException,APIRouter
4
+ from .pydanticModel import *
5
+ from .dao import tableStore
6
+
7
+ # 配置日志
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter()
12
+
13
+ # 定义路由和函数
14
+ @router.post("/login")
15
+ @router.get("/login")
16
+ async def login(login: signIn):
17
+ """用户登录"""
18
+ # ->"user info or throw error"
19
+
20
+ # 检查 ots_client 是否可用
21
+ if router.ots_client is None:
22
+ logger.error("OTS client not initialized")
23
+ raise HTTPException(status_code=500, detail="服务配置错误")
24
+
25
+ # 检查email和password是否匹配
26
+ table_store = tableStore(ots_client=router.ots_client,table_name=router.table_name)
27
+ try:
28
+ login_result = table_store.login(login.email, login.password)
29
+ if login_result['ec']==200:
30
+ logger.info(f"User {login.email} logged in successfully")
31
+ return login_result
32
+ else:
33
+ logger.warning(f"Login failed for {login.email}")
34
+ raise HTTPException(status_code=400, detail="login failed in table store")
35
+ except Exception as e:
36
+ logger.error(f"Login error: {e}")
37
+ print(e)
38
+ raise HTTPException(status_code=400, detail="sign in failed")
39
+
40
+ @router.get("/signUp")
41
+ @router.post("/signUp")
42
+ async def sign_up(signUp: signUp):
43
+ """用户注册"""
44
+ # ->"user info or throw error"
45
+
46
+ # 检查 ots_client 是否可用
47
+ if router.ots_client is None:
48
+ logger.error("OTS client not initialized")
49
+ raise HTTPException(status_code=500, detail="服务配置错误")
50
+
51
+ table_store = tableStore(ots_client=router.ots_client,table_name=router.table_name)
52
+ try:
53
+ signUp_result = table_store.userSignUp(signUp.email,signUp.password)
54
+ if signUp_result['ec']==200:
55
+ logger.info(f"User {signUp.email} registered successfully")
56
+ return signUp_result
57
+ else:
58
+ logger.warning(f"Sign up failed for {signUp.email}")
59
+ raise HTTPException(status_code=400, detail="sign up failed in table store")
60
+ except Exception as e:
61
+ logger.error(f"Sign up error: {e}")
62
+ raise HTTPException(status_code=400, detail="sign up failed")
63
+
64
+ @router.get("/purchase")
65
+ @router.post("/purchase")
66
+ async def purchase(plan: userPlan):
67
+ """用户购买服务"""
68
+ #->bool or raise error
69
+
70
+ # 检查 ots_client 是否可用
71
+ if router.ots_client is None:
72
+ logger.error("OTS client not initialized")
73
+ raise HTTPException(status_code=500, detail="服务配置错误")
74
+
75
+ # 提取plan价格和时间
76
+ price = plan.price
77
+ time_to_add = plan.duration_seconds
78
+ email = plan.email
79
+ password = plan.password
80
+
81
+ # 验证参数
82
+ if price <= 0 or time_to_add <= 0:
83
+ raise HTTPException(status_code=400, detail="Invalid price or duration")
84
+
85
+ table_store = tableStore(ots_client=router.ots_client, table_name=router.table_name)
86
+ # 读取存储数据,如果存的expiredAt>now,那么从expiredAt加时间
87
+ # 如果存的<now ,那么从now加时间 (总之是取两者较大+time_to_add)
88
+
89
+ try:
90
+ table_store.getUserInfo(email=email)
91
+ cur_balance = table_store.balance
92
+ cur_expired_at = table_store.expired_at
93
+ cur_time = int(time.time())
94
+ user_password_stored = table_store.stored_password
95
+
96
+ if cur_balance -price <0:
97
+ logger.warning(f"Insufficient balance for {email}")
98
+ raise HTTPException(status_code=400, detail="balance not enough")
99
+ elif password!=user_password_stored:
100
+ logger.warning(f"Authentication failed for {email}")
101
+ raise HTTPException(status_code=400, detail="auth not pass in purchase")
102
+ else:
103
+ expired_new = max(cur_expired_at, cur_time) + time_to_add
104
+ balance_new = cur_balance -price
105
+ # 更新余额和时间
106
+ update_balance_result = table_store.updateColumnByPrimaryKey(
107
+ key=router.key,
108
+ key_value=email,
109
+ update_column='balance',
110
+ update_column_value=balance_new
111
+ )
112
+ if not update_balance_result:
113
+ logger.error(f"Failed to update balance for {email}")
114
+ raise HTTPException(status_code=400, detail="updateBalance 结果失败")
115
+ update_expired_result = table_store.updateColumnByPrimaryKey(
116
+ key=router.key,
117
+ key_value=email,
118
+ update_column='expiredAt',
119
+ update_column_value=expired_new
120
+ )
121
+ if not update_expired_result:
122
+ logger.error(f"Failed to update expired time for {email}")
123
+ raise HTTPException(status_code=400, detail="update_expired_result 结果失败")
124
+
125
+ logger.info(f"Purchase successful for {email}")
126
+ return True
127
+ except HTTPException:
128
+ raise
129
+ except Exception as e:
130
+ logger.error(f"Purchase error: {e}")
131
+ raise HTTPException(status_code=500, detail="Purchase failed")
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+
148
+
149
+
server/api.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from .notify import router as NotifyRouter
3
+ from .utils import config
4
+ from tablestore import OTSClient
5
+ from .account_manager import router as accountManagerRouter
6
+ import os
7
+
8
+ # 从环境变量读取敏感信息
9
+ access_key_id = os.getenv('ALIYUN_ACCESS_KEY_ID', '')
10
+ access_key_secret = os.getenv('ALIYUN_ACCESS_KEY_SECRET', '')
11
+ end_point = os.getenv('ALIYUN_ENDPOINT', '')
12
+ instance_name = os.getenv('ALIYUN_INSTANCE_NAME', '')
13
+ table_name = 'user'
14
+ primary_key = 'email'
15
+
16
+ # 检查必要的环境变量
17
+ if not all([access_key_id, access_key_secret, end_point, instance_name]):
18
+ print("警告: 缺少必要的阿里云配置环境变量")
19
+ # 使用默认值或抛出错误
20
+ ots_client = None
21
+ else:
22
+ ots_client = OTSClient(
23
+ end_point=end_point,
24
+ access_key_id=access_key_id,
25
+ access_key_secret=access_key_secret,
26
+ instance_name=instance_name
27
+ )
28
+
29
+ accountManagerRouter.key = primary_key
30
+ accountManagerRouter.ots_client = ots_client
31
+ accountManagerRouter.table_name = table_name
32
+
33
+ #和hook.pixiv.digital/uu兼容 故分开
34
+ NotifyRouter.key = primary_key
35
+ NotifyRouter.ots_client = ots_client
36
+ NotifyRouter.table_name = table_name
37
+
38
+ app = FastAPI(
39
+ title="payHook API",
40
+ description="支付回调处理服务",
41
+ version="1.0.0"
42
+ )
43
+
44
+ @app.get("/config.json", tags=["Root"])
45
+ async def read_root() -> dict:
46
+ return config
47
+
48
+ # 全局路由
49
+ app.include_router(accountManagerRouter,prefix='/accountManager')
50
+ app.include_router(NotifyRouter, prefix=config['notify']) # https://hook.pixiv.digital/uu接受回调
51
+
52
+ if __name__ == '__main__':
53
+ print(config['notify'])
54
+
server/config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+
2
+ {
3
+ "notify": "/uu",
4
+ "databaseName": "your_database_name",
5
+ "databaseUser": "your_database_user",
6
+ "password": "your_database_password",
7
+ "host": "your_database_host"
8
+ }
server/dao.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from tablestore import *
3
+
4
+ # 定义一个自定义错误类
5
+ class customError(Exception):
6
+ def __init__(self, message,original_exception=None):
7
+ super().__init__(message)
8
+ self.message = message
9
+ self.original_expection = original_exception
10
+ print("original exception:",original_exception)
11
+
12
+ class tableStore:
13
+ # 使用单例来保证new对象的时候减少负担
14
+ _instance = None
15
+
16
+ def __new__(cls, ots_client, table_name):
17
+ if cls._instance is None:
18
+ print('table store cls 实例为none, 创建新的')
19
+ cls._instance = super(tableStore, cls).__new__(cls)
20
+ cls._instance.ots_client = ots_client
21
+ cls._instance.table_name = table_name
22
+
23
+ return cls._instance
24
+
25
+ def __init__(self,ots_client,table_name):
26
+ self.ots_client = ots_client
27
+ self.table_name = table_name
28
+ self.email = None
29
+ self.stored_password = None
30
+ self.is_vip = None
31
+ self.expired_at = None
32
+ self.balance = None
33
+
34
+ def getUserInfo(self,email)->"void or throw error":
35
+ #通过email主键查询用户信息
36
+ primary_key = [
37
+ ['email', email],
38
+ ]
39
+ columns_to_get = ['isVip', 'expiredAt', 'password','balance']
40
+ try:
41
+ consumed, return_row, next_token = self.ots_client.get_row(
42
+ table_name=self.table_name,
43
+ primary_key=primary_key,
44
+ columns_to_get=columns_to_get
45
+ )
46
+ if return_row is None:
47
+ raise customError('email not exist')
48
+ else:
49
+ # 创建一个字典来存储键值对
50
+ attributes = {}
51
+ for key, value, _ in return_row.attribute_columns:
52
+ attributes[key] = value
53
+
54
+ self.email = email
55
+ self.expired_at = attributes['expiredAt']
56
+ self.is_vip = attributes['isVip']
57
+ self.stored_password = attributes['password']
58
+ self.balance = attributes['balance']
59
+
60
+ except Exception as e:
61
+ raise customError("请求getUserInfo失败",e)
62
+ def checkInit(self)->"bool or throw error":
63
+ # 使用 all() 函数检查所有属性是否都存在且不为 None
64
+ return all([self.email is not None, self.stored_password is not None, self.is_vip is not None,
65
+ self.expired_at is not None])
66
+
67
+ def login(self, email, password)-> "user info or throw error":
68
+ self.getUserInfo(email) #检测登录前先获取用户信息
69
+ if self.checkInit():
70
+ #都存在说明正常初始化
71
+ if self.email==email and self.stored_password==password:
72
+ return {"ec":200,"expiredAt":self.expired_at
73
+ ,"balance":self.balance,"email":email,"password":password}
74
+ else:
75
+ raise customError("login验证失败-账户不匹配")
76
+ else:
77
+ raise customError("login调用时发现用户信息没有正常初始化")
78
+ def checkVipStatus(self)->"bool or throw error":
79
+ current_time = int(time.time())
80
+ if self.checkInit():
81
+ if self.expired_at > current_time:
82
+ #只看截止时间
83
+ return True
84
+ else:
85
+ return False
86
+ else:
87
+ raise customError("checkVipStatus调用时发现用户信息没有正常初始化")
88
+
89
+ def userSignUp(self,email,password)->"user info or throw error":
90
+ #如果收到参数不够,那么务必设置默认值 expiredAt->0 ,isVip->false
91
+ primary_key = [
92
+ ['email', email],
93
+ ]
94
+ attribute_columns = [
95
+ ['isVip', False], # 注册用户不为vip
96
+ ['password', password],
97
+ ['expiredAt', int(time.time())], # 第一次创建过期时间为当前时间
98
+ ['balance', int(0)], # 第一次创建balance为0
99
+ ]
100
+ try:
101
+ self.ots_client.put_row(
102
+ table_name=self.table_name,
103
+ row=Row(
104
+ primary_key=primary_key,
105
+ attribute_columns=attribute_columns
106
+ ),
107
+ condition=Condition(RowExistenceExpectation.EXPECT_NOT_EXIST) # 这种只要存在就不执行(多次注册只有第一次生效)
108
+ )
109
+ return {"ec":200,"expiredAt":0
110
+ ,"balance":0,"email":email,"password":password}
111
+ except Exception as e:
112
+ raise customError("userSignUp 失败",e)
113
+
114
+ def updateColumnByPrimaryKey(self,key:str,key_value:'dynamic',
115
+ update_column:str,update_column_value:'dynamic')\
116
+ -> "bool or throw error":
117
+ #比如支付回调的时候没有密码,故而只根据主键email更新时间
118
+ primary_key = [
119
+ (key, key_value),
120
+ ]
121
+ update_of_attribute_columns = {
122
+ 'PUT': [(update_column, update_column_value)]
123
+ }
124
+ updateRow = Row(
125
+ primary_key=primary_key,
126
+ attribute_columns=update_of_attribute_columns
127
+ )
128
+ try:
129
+ table_meta = self.ots_client.describe_table(table_name=self.table_name)
130
+ defined_columns = table_meta.table_meta.defined_columns # 定义的非主键列-不受意外列添加影响
131
+ isColumnExist = any(column_name == update_column for column_name, _ in defined_columns)
132
+ condition = Condition(RowExistenceExpectation.EXPECT_EXIST)
133
+
134
+ if isColumnExist:
135
+ # 执行UpdateRow操作
136
+ self.ots_client.update_row(
137
+ table_name=self.table_name,
138
+ row=updateRow,
139
+ condition=condition
140
+ )
141
+ return True
142
+ else:
143
+ raise customError("更新列名不存在")
144
+ except Exception as e:
145
+ raise customError('更新操作发生问题',e)
146
+
147
+
148
+
149
+
150
+ if __name__ == '__main__':
151
+ access_key_id = 'LTAI5tNwpEHCMTupADmx6xA4'
152
+ access_key_secret = "vX1q5Tvj3LSsIMUXN6SPbMUBCET3qG"
153
+ end_point = 'https://v2b-user-table.ap-southeast-1.ots.aliyuncs.com'
154
+ instance_name = 'v2b-user-table'
155
+ table_name = 'user'
156
+ ots_client = OTSClient(
157
+ end_point=end_point,
158
+ access_key_id=access_key_id,
159
+ access_key_secret=access_key_secret,
160
+ instance_name=instance_name
161
+ )
162
+ tablestore = tableStore(ots_client=ots_client,table_name=table_name)
163
+ tablestore.updateColumnByPrimaryKey(key='email',key_value='[email protected]',
164
+ update_column='balance',update_column_value=1728877808)
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
server/dao_pool.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mysql.connector
2
+ from mysql.connector import pooling
3
+ from .utils import config
4
+ class Database:
5
+ __connection_pool = None
6
+ @classmethod
7
+ def init(cls):
8
+ try:
9
+ cls.__connection_pool = pooling.MySQLConnectionPool(
10
+ pool_name="v2boardPool",
11
+ pool_size=5,
12
+ host=config["host"],
13
+ database=config["databaseName"],
14
+ user=config["databaseUser"],
15
+ password=config["password"]
16
+ )
17
+ except mysql.connector.Error as e:
18
+ print("Error initializing connection pool: {}".format(str(e)))
19
+
20
+ @classmethod
21
+ def get_connection(cls):
22
+ if cls.__connection_pool is None:
23
+ cls.init()
24
+ conn = cls.__connection_pool.get_connection()
25
+ return conn
26
+
27
+ @classmethod
28
+ def release_connection(cls, connection):
29
+ connection.close()
30
+
31
+ @classmethod
32
+ def close_all_connections(cls):
33
+ cls.__connection_pool.close()
34
+
35
+ @classmethod
36
+ def status(cls):
37
+ # 获取使用了的连接状态
38
+ if cls.__connection_pool is None:
39
+ cls.init()
40
+ return cls.__connection_pool.get_pool_size()
41
+
42
+ if __name__ == '__main__':
43
+ email = '[email protected]'
44
+ with Database.get_connection() as conn:
45
+ with conn.cursor() as cur:
46
+ print("链接数据库成功")
47
+ query = "SELECT balance FROM `v2_user` WHERE email = %(email)s"
48
+ params = {'email': email}
49
+ cur.execute(query, params)
50
+ result = cur.fetchone()
51
+
52
+ if result is not None:
53
+ # 如果email已经存在于数据库中,那么从数据库中获取balance
54
+ balance = result[0]
55
+ print(balance)
56
+ else:
57
+ # 如果email不存在于数据库中,提示用户未注册(没必要,因为没法通知到app),所以不做处理
58
+ pass
59
+ print("数据库操作完成")
server/hook.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #notify通知到后自动写入数据库的hook操作,只添加余额就行,连月都不用管
2
+ from .dao_pool import Database
3
+ def updateBalance(email,amount):
4
+ ##通过传入用户email和回调中的金额修改balance
5
+ with Database.get_connection() as conn:
6
+ with conn.cursor() as cur:
7
+ print("链接数据库成功")
8
+ query = "SELECT balance FROM `v2_user` WHERE email = %(email)s"
9
+ params = {'email': email}
10
+ cur.execute(query, params)
11
+ result = cur.fetchone()
12
+
13
+ if result is not None:
14
+ # 如果email已经存在于数据库中,那么从数据库中获取balance,让balance加上新充值的金额就行
15
+ balance = result[0]
16
+ balance +=float(amount)*100
17
+ print(balance)
18
+ query = "UPDATE `v2_user` SET balance = %(balance)s WHERE email = %(email)s"
19
+ params = {'balance': str(balance), 'email': email}
20
+ cur.execute(query, params)
21
+ conn.commit()
22
+ else:
23
+ # 如果email不存在于数据库中,提示用户未注册(没必要,因为没法通知到app),所以不做处理
24
+ pass
25
+ print("数据库操作完成,当前email: ",email)
26
+
27
+
28
+
29
+
server/notify.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from fastapi import APIRouter, HTTPException, Request
3
+ from fastapi.responses import JSONResponse
4
+ from .hook import *
5
+ from .pydanticModel import *
6
+ from .dao import tableStore
7
+ import logging
8
+
9
+ # 配置日志
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ router = APIRouter()
14
+
15
+ #afdian的回调是post huggingfaceb不知道抽什么风只能二级路由
16
+ @router.get('/uu',response_model=AfdianResp)
17
+ @router.post('/uu',response_model=AfdianResp)
18
+ async def update_balance(afdianHookjson: AfdianHookJson):
19
+ """处理爱发电支付回调"""
20
+
21
+ resp = {'ec': 200}
22
+
23
+ # 检查 ots_client 是否可用
24
+ if router.ots_client is None:
25
+ logger.error("OTS client not initialized")
26
+ return {'ec': "服务配置错误"}
27
+
28
+ # 检查 ec 是否为 200
29
+ if afdianHookjson.ec != 200:
30
+ logger.warning(f"Invalid ec value: {afdianHookjson.ec}")
31
+ return {'ec': "afdian hook错误,ec 不是 200"}
32
+
33
+ # 提取订单详情和可选参数
34
+ order_details = afdianHookjson.data.order
35
+ custom_order_id = order_details.custom_order_id
36
+ total_amount = order_details.total_amount
37
+
38
+ # 验证必要参数
39
+ if not custom_order_id:
40
+ logger.warning("Missing custom_order_id")
41
+ return {'ec': "缺少用户标识"}
42
+
43
+ table_store = tableStore(ots_client=router.ots_client, table_name=router.table_name)
44
+
45
+ if all([custom_order_id is not None, total_amount is not None]):
46
+ # 说明是余额类型
47
+ try:
48
+ # 先给mysql的数据库添加 - 这个毕竟久 稳定 但是不应该影响其他数据库添加(万一不用了)--------------------
49
+ updateBalance(email=custom_order_id, amount=total_amount)
50
+ # 先给mysql的数据库添加 - 这个毕竟久 稳定 但是不应该影响其他数据库添加(万一不用了)--------------------
51
+ except Exception as e:
52
+ logger.error(f"MySQL update failed: {e}")
53
+ print("updateBalance v2b digitalocean mysql failed",e)
54
+
55
+ try:
56
+ #应该改成从原来的余额基础上加total_amount的值
57
+ table_store.getUserInfo(email=custom_order_id)
58
+ cur_balance = table_store.balance
59
+ balance_new = cur_balance+ total_amount
60
+ # 更新余额列
61
+ update_balance_result = table_store.updateColumnByPrimaryKey(
62
+ key=router.key,
63
+ key_value=custom_order_id,
64
+ update_column='balance',
65
+ update_column_value=balance_new
66
+ )
67
+ if update_balance_result:
68
+ logger.info(f"Successfully updated balance for {custom_order_id}")
69
+ return resp #全部成功运行则返回爱发电要求的ec =200
70
+ else:
71
+ logger.error(f"Failed to update balance for {custom_order_id}")
72
+ return {'ec': "updateBalance tablestore 结果失败"}
73
+ except Exception as e:
74
+ logger.error(f"TableStore update failed: {e}")
75
+ print(e)
76
+ return {'ec': "尝试 updateBalance tablestore 失败"}
77
+ else:
78
+ #这个直接返回200吧,反正测试接口的时候需要 ,平时也不用到
79
+ # return {'ec': "afdian hook custom_order_id 或者 total_amount 为 None"}
80
+ logger.info("Test callback received")
81
+ return resp
82
+
83
+ # test
84
+ # @router.post('/uu')
85
+ # async def prinf_test_json(request: Request):
86
+ # json_data = await request.json()
87
+ # print("收到afdian请求:", json_data)
88
+ # return {'ec': 200}
server/pydanticModel.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel,field_validator,Field
2
+ from fastapi import Query
3
+ from typing import List, Optional, Any
4
+
5
+
6
+ # 定义fastapi请求参数模型
7
+
8
+ class signUp(BaseModel):
9
+ email: str
10
+ password: str
11
+ # 可选参数
12
+ expiredAt: int = Query(default=0, alias="expiredAt")
13
+ balance: int = Query(default=0, alias="balance")
14
+ ec: int = Query(default=200, alias="ec")
15
+
16
+
17
+ @field_validator('email')
18
+ #确保password不为''
19
+ def check_email(cls, v):
20
+ if v == '':
21
+ raise ValueError('email cannot be an empty string')
22
+ return v
23
+
24
+ @field_validator('password')
25
+ # 确保password不为''
26
+ def check_password(cls, v):
27
+ if v == '':
28
+ raise ValueError('password cannot be an empty string')
29
+ return v
30
+
31
+
32
+ class signIn(BaseModel):
33
+ email: str
34
+ password: str
35
+ # 可选参数
36
+ expiredAt: int = Query(default=0, alias="expiredAt")
37
+ balance: int = Query(default=0, alias="balance")
38
+ ec: int = Query(default=200, alias="ec")
39
+
40
+
41
+ @field_validator('email')
42
+ # 确保password不为''
43
+ def check_email(cls, v):
44
+ if v == '':
45
+ raise ValueError('email cannot be an empty string')
46
+ return v
47
+
48
+ @field_validator('password')
49
+ # 确保password不为''
50
+ def check_password(cls, v):
51
+ if v == '':
52
+ raise ValueError('password cannot be an empty string')
53
+ return v
54
+
55
+
56
+ class SKUDetail(BaseModel):
57
+ sku_id: Optional[str] = None
58
+ price: Optional[str] = None
59
+ count: Optional[int] = None
60
+ name: Optional[str] = None
61
+ album_id: Optional[str] = None
62
+ pic: Optional[str] = None
63
+ stock: Optional[str] = None
64
+ post_id: Optional[str] = None
65
+
66
+
67
+ class AfdianOrderDetail(BaseModel):
68
+ out_trade_no: Optional[str] = Field(default=None, alias="out_trade_no")
69
+ user_id: Optional[str] = Field(default=None, alias="user_id")
70
+ plan_id: Optional[str] = Field(default=None, alias="plan_id")
71
+ title: Optional[str] = Field(default=None, alias="title") #just afdian test have
72
+ month: Optional[int] = Field(default=None, alias="month")
73
+ total_amount: Optional[int] = Field(default=None, alias="total_amount") # 这里自动转int了
74
+ show_amount: Optional[str] = Field(default=None, alias="show_amount")
75
+ status: Optional[int] = Field(default=None, alias="status")
76
+ remark: Optional[str] = Field(default=None, alias="remark")
77
+ redeem_id: Optional[str] = Field(default=None, alias="redeem_id")
78
+ product_type: Optional[int] = Field(default=None, alias="product_type")
79
+ discount: Optional[str] = Field(default=None, alias="discount")
80
+ sku_detail: Optional[List[SKUDetail]] = Field(default=[], alias="sku_detail") #兼容afdian test 请求
81
+ create_time: Optional[int] = Field(default=None, alias="create_time")
82
+ plan_title: Optional[str] = Field(default=None, alias="plan_title")
83
+ user_private_id: Optional[str] = Field(default=None, alias="user_private_id")
84
+ address_person: Optional[str] = Field(default=None, alias="address_person")
85
+ address_phone: Optional[str] = Field(default=None, alias="address_phone")
86
+ address_address: Optional[str] = Field(default=None, alias="address_address")
87
+ custom_order_id: Optional[str] = Field(default=None, alias="custom_order_id")
88
+
89
+
90
+ class AfdianOrderData(BaseModel):
91
+ type: str
92
+ order: AfdianOrderDetail
93
+
94
+
95
+ class AfdianHookJson(BaseModel):
96
+ ec: int
97
+ em: str
98
+ data: AfdianOrderData
99
+
100
+
101
+ class AfdianResp(BaseModel):
102
+ ec: Any
103
+
104
+
105
+
106
+ #根据plan中的价格和时间相应减少balance和增加expiredAt
107
+ class userPlan(BaseModel):
108
+ price: int
109
+ duration_seconds: int
110
+ email: str #订阅plan的用户
111
+ password: str #请求purchase接口用户的password,保证只有用户本身可以请求这个接口防止滥用
112
+
113
+ @field_validator('price')
114
+ # 确保password不为''
115
+ def check_price(cls, v):
116
+ if v < 0:
117
+ raise ValueError('price can not < 0')
118
+ return v
119
+
120
+ @field_validator('duration_seconds')
121
+ # 确保password不为''
122
+ def check_duration_seconds(cls, v):
123
+ if v < 0:
124
+ raise ValueError('duration_seconds can not < 0')
125
+ return v
126
+
127
+ @field_validator('email')
128
+ # 确保password不为''
129
+ def check_email(cls, v):
130
+ if v == '':
131
+ raise ValueError('email cannot be an empty string')
132
+ return v
133
+
134
+
135
+
136
+
137
+
server/utils.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ def load_config():
5
+ """从配置文件和环境变量加载配置"""
6
+ config_path = os.path.join(os.path.dirname(__file__), 'config.json')
7
+
8
+ # 读取基础配置
9
+ with open(config_path, 'r', encoding='utf-8') as f:
10
+ config = json.load(f)
11
+
12
+ # 从环境变量覆盖敏感信息
13
+ config['databaseName'] = os.getenv('MYSQL_DATABASE', config['databaseName'])
14
+ config['databaseUser'] = os.getenv('MYSQL_USER', config['databaseUser'])
15
+ config['password'] = os.getenv('MYSQL_PASSWORD', config['password'])
16
+ config['host'] = os.getenv('MYSQL_HOST', config['host'])
17
+
18
+ return config
19
+
20
+ config = load_config()
21
+
22
+ if __name__ == '__main__':
23
+ print(config['notify'])
test.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ # port = 7860
3
+ port= 3000
4
+
5
+ # url = "http://localhost:{p}/accountManager/login".format(p=port)
6
+ url = "http://localhost:{p}/accountManager/signUp".format(p=port)
7
+ data = {
8
+ "email": '[email protected]',
9
+ "password": '[email protected]'
10
+ }
11
+ response = requests.get(url, json=data)
12
+ print(response.status_code)
13
+ print(response.json())
14
+
15
+
16
+ # #hook模拟
17
+ # # port = 7860
18
+ # # url = "http://localhost:{p}/uu".format(p=port)
19
+ # url = 'https://hook.pixiv.digital/uu'
20
+ # data = {
21
+ # "ec": 200,
22
+ # "em": "ok",
23
+ # "data": {
24
+ # "type": "order",
25
+ # "order": {
26
+ # "out_trade_no": "20241014133823102101567665",
27
+ # "user_id": "f05ca070734c11efa78052540025c377",
28
+ # "plan_id": "ef3c43c6daa411ee965e5254001e7c00",
29
+ # "month": 1,
30
+ # "total_amount": "10.00",
31
+ # "show_amount": "10.00",
32
+ # "status": 2,
33
+ # "remark": "",
34
+ # "redeem_id": "",
35
+ # "product_type": 1,
36
+ # "discount": "0.00",
37
+ # "sku_detail": [
38
+ # {
39
+ # "sku_id": "ef440afcdaa411ee9e165254001e7c00",
40
+ # "price": "1.00",
41
+ # "count": 10,
42
+ # "name": "\u4f59\u989d\u6570\u91cf",
43
+ # "album_id": "",
44
+ # "pic": "",
45
+ # "stock": "",
46
+ # "post_id": ""
47
+ # }
48
+ # ],
49
+ # "create_time": 1728884303,
50
+ # "plan_title": "\u4f59\u989d",
51
+ # "user_private_id": "f14e880d78ac9d5c623b677c6e0c7122f1158bf0",
52
+ # "address_person": "",
53
+ # "address_phone": "",
54
+ # "address_address": "",
55
+ # "custom_order_id": "[email protected]"
56
+ # }
57
+ # }
58
+ # }
59
+ # response = requests.get(url, json=data)
60
+ # print(response.status_code)
61
+ # print(response.text)
62
+
63
+
64
+
65
+ # # purchase 模拟
66
+ # url = "http://localhost:{p}/accountManager/purchase".format(p=port)
67
+ # data = {
68
+ # "price":10,
69
+ # "duration_seconds":2592000,
70
+ # "email":'[email protected]',
71
+ # "password":"[email protected]"
72
+ # }
73
+ #
74
+ # response = requests.get(url, json=data)
75
+ # print(response.status_code)
76
+ # print(response.json())