feat: 初始化工程
This commit is contained in:
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
# Python 字节码文件
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C 扩展
|
||||
*.so
|
||||
|
||||
# 分发/打包
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# 虚拟环境
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.env
|
||||
.venv
|
||||
|
||||
# 测试
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Django 相关
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media/
|
||||
|
||||
# PyCharm IDE
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
.history/
|
||||
|
||||
# 其他
|
||||
.DS_Store
|
||||
|
||||
logs/
|
||||
packages/
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/patch-service.iml" filepath="$PROJECT_DIR$/.idea/patch-service.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/patch-service.iml
generated
Normal file
10
.idea/patch-service.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (patch-service)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
124
.idea/workspace.xml
generated
Normal file
124
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="92592c29-2276-4c2c-bd45-9ab94e25491d" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/patch-service.iml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/config/database.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/config/rustfs.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/models/base.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/models/patch.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/routers/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/routers/crop.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/routers/file.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/routers/patch.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/routers/record.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/schemas/patch.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/service/crop_service.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/service/file_service.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/service/patch_service.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/service/record_service.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/test_main.http" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/utils/common.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="FileTemplateManagerImpl">
|
||||
<option name="RECENT_TEMPLATES">
|
||||
<list>
|
||||
<option value="FastAPI main" />
|
||||
<option value="FastAPI test_main" />
|
||||
<option value="Python Script" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"associatedIndex": 7
|
||||
}]]></component>
|
||||
<component name="ProjectId" id="3J4P6AjqhDBRxWCujlbOFJb7BqN" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
||||
<ConfirmationsSetting value="2" id="Add" />
|
||||
</component>
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"FastAPI.patch-service.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"last_opened_file_path": "D:/Cxx/PythonProjects/patch-service/routers",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="D:\Cxx\PythonProjects\patch-service\routers" />
|
||||
<recent name="D:\Cxx\PythonProjects\patch-service\service" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="patch-service" type="Python.FastAPI">
|
||||
<option name="file" value="$PROJECT_DIR$/main.py" />
|
||||
<module name="patch-service" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="PARENT_ENVS" value="true" />
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="IS_MODULE_SDK" value="false" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
<option name="launchJavascriptDebuger" value="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-PY-243.26053.29" />
|
||||
<option value="bundled-python-sdk-b1dbf8ef85a6-4df51de95216-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-243.26053.29" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="92592c29-2276-4c2c-bd45-9ab94e25491d" name="Changes" comment="" />
|
||||
<created>1788917189828</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1788917189828</updated>
|
||||
<workItem from="1788917190899" duration="17315000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||
<SUITE FILE_PATH="coverage/patch_service$patch_service.coverage" NAME="patch-service Coverage Results" MODIFIED="1788950175309" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
|
||||
</component>
|
||||
</project>
|
||||
26
config/database.py
Normal file
26
config/database.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
load_dotenv()
|
||||
|
||||
DATABASE_URL = f"mysql+pymysql://root:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:3306/patch"
|
||||
|
||||
engine = create_engine(url=DATABASE_URL, pool_pre_ping=True, pool_recycle=3600)
|
||||
|
||||
# 会话工厂
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
# ORM基类
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
# 创建数据库会话实例
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
18
config/rustfs.py
Normal file
18
config/rustfs.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
access_key = 'Qc0pkBUAs5zEGgwWbYxr'
|
||||
secret_access = '9LQmdBYlagrCh5Iq0K1264UHRzfZ7VeFiDNGvsnb'
|
||||
|
||||
s3 = boto3.client('s3',
|
||||
endpoint_url=f'http://{os.getenv("RUSTFS_HOST")}:{os.getenv("RUSTFS_PORT")}',
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_access,
|
||||
config=Config(signature_version='s3v4'),
|
||||
region_name='cn-east-1'
|
||||
)
|
||||
0
id_generator/__init__.py
Normal file
0
id_generator/__init__.py
Normal file
38
id_generator/generator.py
Normal file
38
id_generator/generator.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
雪花算法生成器IdGenerator
|
||||
"""
|
||||
|
||||
# !/usr/bin/python
|
||||
# coding=UTF-8
|
||||
|
||||
|
||||
from . import options
|
||||
from . import snowflake_m1
|
||||
|
||||
|
||||
class DefaultIdGenerator:
|
||||
"""
|
||||
ID生成器
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.snowflake = None
|
||||
|
||||
def set_id_generator(self, option: options.IdGeneratorOptions):
|
||||
"""
|
||||
设置id生成规则信息
|
||||
"""
|
||||
|
||||
if option.base_time < 100000:
|
||||
raise ValueError("base time error.")
|
||||
|
||||
self.snowflake = snowflake_m1.SnowFlakeM1(option)
|
||||
|
||||
def next_id(self) -> int:
|
||||
"""
|
||||
获取新的UUID
|
||||
"""
|
||||
|
||||
if self.snowflake is None:
|
||||
raise ValueError("please set id generator at first.")
|
||||
return self.snowflake.next_id()
|
||||
134
id_generator/idregister.py
Normal file
134
id_generator/idregister.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
worker id generator
|
||||
"""
|
||||
|
||||
# !/usr/bin/python
|
||||
# coding=UTF-8
|
||||
|
||||
|
||||
from threading import Thread
|
||||
import time
|
||||
import logging
|
||||
import redis
|
||||
|
||||
|
||||
class Register:
|
||||
"""
|
||||
redis封装
|
||||
- host 代表redis ip
|
||||
- port 代表redis端口
|
||||
- max_worker_id worker_id的最大值, 默认为100
|
||||
- password redis的密码, 默认为空
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, max_worker_id=100, password=None):
|
||||
self.redis_impl = redis.StrictRedis(host=host, port=port, db=0, password=password)
|
||||
self.loop_count = 0
|
||||
self.max_loop_count = 10
|
||||
self.worker_id_expire_time = 15
|
||||
self.max_worker_id = max_worker_id
|
||||
self.worker_id = -1
|
||||
self.is_stop = False
|
||||
|
||||
def get_lock(self, key):
|
||||
"""
|
||||
获取分布式全局锁,并设置过期时间为30秒
|
||||
"""
|
||||
|
||||
if self.redis_impl.setnx(key, 1):
|
||||
self.redis_impl.expire(key, 30)
|
||||
return True
|
||||
if self.redis_impl.ttl(key) < 0:
|
||||
self.redis_impl.expire(key, 30)
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
退出注册器的线程
|
||||
"""
|
||||
|
||||
self.is_stop = True
|
||||
|
||||
def get_worker_id(self):
|
||||
"""
|
||||
获取全局唯一worker_id, 会创建一个线程给worker id续期
|
||||
失败返回-1
|
||||
"""
|
||||
|
||||
self.loop_count = 0
|
||||
|
||||
def extern_life(my_id):
|
||||
while 1:
|
||||
time.sleep(self.worker_id_expire_time / 3)
|
||||
# 是否关闭了
|
||||
if self.is_stop:
|
||||
return
|
||||
# 更新生命周期
|
||||
if self.worker_id != my_id:
|
||||
break
|
||||
try:
|
||||
self.redis_impl.expire(
|
||||
f"IdGen:WorkerId:Value:{my_id}",
|
||||
self.worker_id_expire_time)
|
||||
except Exception as exe:
|
||||
logging.error(exe)
|
||||
continue
|
||||
|
||||
self.worker_id = self.__get_next_worker_id()
|
||||
if self.worker_id > -1:
|
||||
Thread(target=extern_life, args=[self.worker_id]).start()
|
||||
return self.worker_id
|
||||
|
||||
def __get_next_worker_id(self):
|
||||
"""
|
||||
获取全局唯一worker id内部实现
|
||||
"""
|
||||
|
||||
cur = self.redis_impl.incrby("IdGen:WorkerId:Index", 1)
|
||||
|
||||
def can_reset():
|
||||
try:
|
||||
reset_value = self.redis_impl.incr("IdGen:WorkerId:Value:Edit")
|
||||
return reset_value != 1
|
||||
except Exception as ept:
|
||||
logging.error(ept)
|
||||
return False
|
||||
|
||||
def end_reset():
|
||||
try:
|
||||
self.redis_impl.set("IdGen:WorkerId:Value:Edit", 0)
|
||||
except Exception as ept:
|
||||
logging.error(ept)
|
||||
|
||||
def is_available(worker_id: int):
|
||||
try:
|
||||
rst = self.redis_impl.get(f"IdGen:WorkerId:Value:{worker_id}")
|
||||
return rst != "Y"
|
||||
except Exception as ept:
|
||||
logging.error(ept)
|
||||
return False
|
||||
|
||||
if cur > self.max_worker_id:
|
||||
if can_reset():
|
||||
self.redis_impl.set("IdGen:WorkerId:Index", -1)
|
||||
end_reset()
|
||||
self.loop_count += 1
|
||||
|
||||
if self.loop_count > self.max_loop_count:
|
||||
self.loop_count = 0
|
||||
return -1
|
||||
|
||||
time.sleep(0.2 * self.loop_count)
|
||||
return self.__get_next_worker_id()
|
||||
time.sleep(0.2)
|
||||
return self.__get_next_worker_id()
|
||||
if is_available(cur):
|
||||
self.redis_impl.setex(
|
||||
f"IdGen:WorkerId:Value:{cur}",
|
||||
self.worker_id_expire_time,
|
||||
"Y"
|
||||
)
|
||||
self.loop_count = 0
|
||||
return cur
|
||||
|
||||
return self.__get_next_worker_id()
|
||||
43
id_generator/options.py
Normal file
43
id_generator/options.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
生成器IdGenerator配置选项
|
||||
"""
|
||||
|
||||
# !/usr/bin/python
|
||||
# coding=UTF-8
|
||||
|
||||
|
||||
class IdGeneratorOptions:
|
||||
"""
|
||||
ID生成器配置
|
||||
- worker_id 全局唯一id, 区分不同uuid生成器实例
|
||||
- worker_id_bit_length 生成的uuid中worker_id占用的位数
|
||||
- seq_bit_length 生成的uuid中序列号占用的位数
|
||||
"""
|
||||
|
||||
def __init__(self, worker_id=0, worker_id_bit_length=6, seq_bit_length=6):
|
||||
|
||||
# 雪花计算方法,(1-漂移算法|2-传统算法), 默认1。目前只实现了1。
|
||||
self.method = 1
|
||||
|
||||
# 基础时间(ms单位), 不能超过当前系统时间
|
||||
self.base_time = 1582136402000
|
||||
|
||||
# 机器码, 必须由外部设定, 最大值 2^worker_id_bit_length-1
|
||||
self.worker_id = worker_id
|
||||
|
||||
# 机器码位长, 默认值6, 取值范围 [1, 15](要求:序列数位长+机器码位长不超过22)
|
||||
self.worker_id_bit_length = worker_id_bit_length
|
||||
|
||||
# 序列数位长, 默认值6, 取值范围 [3, 21](要求:序列数位长+机器码位长不超过22)
|
||||
self.seq_bit_length = seq_bit_length
|
||||
|
||||
# 最大序列数(含), 设置范围 [max_seq_number, 2^seq_bit_length-1]
|
||||
# 默认值0, 表示最大序列数取最大值(2^seq_bit_length-1])
|
||||
self.max_seq_number = 0
|
||||
|
||||
# 最小序列数(含), 默认值5, 取值范围 [5, max_seq_number], 每毫秒的前5个序列数对应编号0-4是保留位
|
||||
# 其中1-4是时间回拨相应预留位, 0是手工新值预留位
|
||||
self.min_seq_number = 5
|
||||
|
||||
# 最大漂移次数(含), 默认2000, 推荐范围500-10000(与计算能力有关)
|
||||
self.top_over_cost_count = 2000
|
||||
20
id_generator/snowflake.py
Normal file
20
id_generator/snowflake.py
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
"""
|
||||
雪花算法生成器接口声明
|
||||
"""
|
||||
|
||||
# !/usr/bin/python
|
||||
# coding=UTF-8
|
||||
|
||||
|
||||
class SnowFlake():
|
||||
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
|
||||
def next_id(self) -> int:
|
||||
"""
|
||||
获取新的UUID
|
||||
"""
|
||||
|
||||
return 0
|
||||
147
id_generator/snowflake_m1.py
Normal file
147
id_generator/snowflake_m1.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
M1生成器
|
||||
"""
|
||||
|
||||
# !/usr/bin/python
|
||||
# coding=UTF-8
|
||||
|
||||
import threading
|
||||
import time
|
||||
from .snowflake import SnowFlake
|
||||
from .options import IdGeneratorOptions
|
||||
|
||||
|
||||
class SnowFlakeM1(SnowFlake):
|
||||
"""
|
||||
M1规则ID生成器配置
|
||||
"""
|
||||
|
||||
def __init__(self, options: IdGeneratorOptions):
|
||||
# 1.base_time
|
||||
self.base_time = 1582136402000
|
||||
if options.base_time != 0:
|
||||
self.base_time = int(options.base_time)
|
||||
|
||||
# 2.worker_id_bit_length
|
||||
self.worker_id_bit_length = 6
|
||||
if options.worker_id_bit_length != 0:
|
||||
self.worker_id_bit_length = int(options.worker_id_bit_length)
|
||||
|
||||
# 3.worker_id
|
||||
self.worker_id = options.worker_id
|
||||
|
||||
# 4.seq_bit_length
|
||||
self.seq_bit_length = 6
|
||||
if options.seq_bit_length != 0:
|
||||
self.seq_bit_length = int(options.seq_bit_length)
|
||||
|
||||
# 5.max_seq_number
|
||||
self.max_seq_number = int(options.max_seq_number)
|
||||
if options.max_seq_number <= 0:
|
||||
self.max_seq_number = (1 << self.seq_bit_length) - 1
|
||||
|
||||
# 6.min_seq_number
|
||||
self.min_seq_number = int(options.min_seq_number)
|
||||
|
||||
# 7.top_over_cost_count
|
||||
self.top_over_cost_count = int(options.top_over_cost_count)
|
||||
|
||||
# 8.Others
|
||||
self.__timestamp_shift = self.worker_id_bit_length + self.seq_bit_length
|
||||
self.__current_seq_number = self.min_seq_number
|
||||
self.__last_time_tick: int = 0
|
||||
self.__turn_back_time_tick: int = 0
|
||||
self.__turn_back_index: int = 0
|
||||
self.__is_over_cost = False
|
||||
self.___over_cost_count_in_one_term: int = 0
|
||||
self.__id_lock = threading.Lock()
|
||||
|
||||
def __next_over_cost_id(self) -> int:
|
||||
current_time_tick = self.__get_current_time_tick()
|
||||
if current_time_tick > self.__last_time_tick:
|
||||
self.__last_time_tick = current_time_tick
|
||||
self.__current_seq_number = self.min_seq_number
|
||||
self.__is_over_cost = False
|
||||
self.___over_cost_count_in_one_term = 0
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
if self.___over_cost_count_in_one_term >= self.top_over_cost_count:
|
||||
self.__last_time_tick = self.__get_next_time_tick()
|
||||
self.__current_seq_number = self.min_seq_number
|
||||
self.__is_over_cost = False
|
||||
self.___over_cost_count_in_one_term = 0
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
if self.__current_seq_number > self.max_seq_number:
|
||||
self.__last_time_tick += 1
|
||||
self.__current_seq_number = self.min_seq_number
|
||||
self.__is_over_cost = True
|
||||
self.___over_cost_count_in_one_term += 1
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
def __next_normal_id(self) -> int:
|
||||
current_time_tick = self.__get_current_time_tick()
|
||||
if current_time_tick < self.__last_time_tick:
|
||||
if self.__turn_back_time_tick < 1:
|
||||
self.__turn_back_time_tick = self.__last_time_tick - 1
|
||||
self.__turn_back_index += 1
|
||||
# 每毫秒序列数的前5位是预留位, 0用于手工新值, 1-4是时间回拨次序
|
||||
# 支持4次回拨次序(避免回拨重叠导致ID重复), 可无限次回拨(次序循环使用)。
|
||||
if self.__turn_back_index > 4:
|
||||
self.__turn_back_index = 1
|
||||
|
||||
return self.__calc_turn_back_id(self.__turn_back_time_tick)
|
||||
|
||||
# 时间追平时, _TurnBackTimeTick清零
|
||||
self.__turn_back_time_tick = min(self.__turn_back_time_tick, 0)
|
||||
|
||||
if current_time_tick > self.__last_time_tick:
|
||||
self.__last_time_tick = current_time_tick
|
||||
self.__current_seq_number = self.min_seq_number
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
if self.__current_seq_number > self.max_seq_number:
|
||||
self.__last_time_tick += 1
|
||||
self.__current_seq_number = self.min_seq_number
|
||||
self.__is_over_cost = True
|
||||
self.___over_cost_count_in_one_term = 1
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
return self.__calc_id(self.__last_time_tick)
|
||||
|
||||
def __calc_id(self, use_time_tick) -> int:
|
||||
self.__current_seq_number += 1
|
||||
return (
|
||||
(use_time_tick << self.__timestamp_shift) +
|
||||
(self.worker_id << self.seq_bit_length) +
|
||||
self.__current_seq_number
|
||||
) % int(1e64)
|
||||
|
||||
def __calc_turn_back_id(self, use_time_tick) -> int:
|
||||
self.__turn_back_time_tick -= 1
|
||||
return (
|
||||
(use_time_tick << self.__timestamp_shift) +
|
||||
(self.worker_id << self.seq_bit_length) +
|
||||
self.__turn_back_index
|
||||
) % int(1e64)
|
||||
|
||||
def __get_current_time_tick(self) -> int:
|
||||
return int((time.time_ns() / 1e6) - self.base_time)
|
||||
|
||||
def __get_next_time_tick(self) -> int:
|
||||
temp_time_ticker = self.__get_current_time_tick()
|
||||
while temp_time_ticker <= self.__last_time_tick:
|
||||
# 0.001 = 1 mili sec
|
||||
time.sleep(0.001)
|
||||
temp_time_ticker = self.__get_current_time_tick()
|
||||
return temp_time_ticker
|
||||
|
||||
def next_id(self) -> int:
|
||||
with self.__id_lock:
|
||||
if self.__is_over_cost:
|
||||
nextid = self.__next_over_cost_id()
|
||||
else:
|
||||
nextid = self.__next_normal_id()
|
||||
return nextid
|
||||
16
main.py
Normal file
16
main.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from routers import routers
|
||||
|
||||
app = FastAPI(title="Patch Service")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
for router in routers:
|
||||
app.include_router(router)
|
||||
41
models/base.py
Normal file
41
models/base.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from sqlalchemy import Column, BigInteger, DateTime, event
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
|
||||
from config.database import Base
|
||||
from datetime import datetime
|
||||
|
||||
from utils.common import camel_to_snake
|
||||
from id_generator import options, generator
|
||||
|
||||
# https://github.com/yitter/IdGenerator/tree/master/Python
|
||||
options = options.IdGeneratorOptions(worker_id=23)
|
||||
idgen = generator.DefaultIdGenerator()
|
||||
idgen.set_id_generator(options)
|
||||
|
||||
|
||||
# 第二层基类:包含ID
|
||||
class IdBase(Base):
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True)
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
# 自动把数据库实体类名驼峰转为数据库表名下划线
|
||||
return camel_to_snake(cls.__name__)
|
||||
|
||||
|
||||
# 自动填充id
|
||||
@event.listens_for(IdBase, 'before_insert', propagate=True)
|
||||
def before_insert_listener(mapper, connection, target):
|
||||
if target.id is None:
|
||||
target.id = idgen.next_id()
|
||||
|
||||
|
||||
# 第二层基类:包含ID和审计字段
|
||||
class AuditBase(IdBase):
|
||||
__abstract__ = True
|
||||
|
||||
create_time = Column(DateTime, nullable=True, default=datetime.now)
|
||||
update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
26
models/patch.py
Normal file
26
models/patch.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from sqlalchemy import Column, JSON, String, Text, BigInteger, Date
|
||||
|
||||
from models.base import AuditBase
|
||||
|
||||
|
||||
class Patches(AuditBase):
|
||||
name = Column(String(100), nullable=False)
|
||||
location = Column(String(200))
|
||||
status = Column(String(50))
|
||||
|
||||
|
||||
class Crops(AuditBase):
|
||||
patch_id = Column(BigInteger, nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
variety = Column(String(100))
|
||||
date = Column(Date)
|
||||
|
||||
|
||||
class Records(AuditBase):
|
||||
crop_id = Column(BigInteger, nullable=False)
|
||||
type = Column(String(50))
|
||||
date = Column(Date)
|
||||
content = Column(Text)
|
||||
images = Column(JSON)
|
||||
videos = Column(JSON)
|
||||
stage = Column(String(50))
|
||||
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
fastapi~=0.140.0
|
||||
python-dotenv~=1.2.2
|
||||
requests~=2.34.2
|
||||
starlette~=1.3.1
|
||||
pydantic~=2.13.4
|
||||
SQLAlchemy~=2.0.51
|
||||
asyncpg~=0.30.0
|
||||
uvicorn~=0.23.0
|
||||
pymysql~=1.2.0
|
||||
boto3~=1.40.59
|
||||
botocore~=1.40.59
|
||||
python-multipart~=0.0.20
|
||||
11
routers/__init__.py
Normal file
11
routers/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .patch import router as patch_router
|
||||
from .crop import router as crop_router
|
||||
from .record import router as record_router
|
||||
from .file import router as file_router
|
||||
|
||||
routers = [
|
||||
patch_router,
|
||||
crop_router,
|
||||
record_router,
|
||||
file_router,
|
||||
]
|
||||
29
routers/crop.py
Normal file
29
routers/crop.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import get_db
|
||||
from schemas.patch import CropCreate, CropUpdate
|
||||
from service import crop_service
|
||||
|
||||
router = APIRouter(prefix="/crops", tags=["crops"])
|
||||
|
||||
|
||||
@router.post("", response_model=bool)
|
||||
def create(data: CropCreate, db: Session = Depends(get_db)):
|
||||
return crop_service.create_crop(db, data)
|
||||
|
||||
|
||||
@router.put("/{crop_id}", response_model=bool)
|
||||
def update(crop_id: int, data: CropUpdate, db: Session = Depends(get_db)):
|
||||
crop = crop_service.update_crop(db, crop_id, data)
|
||||
if not crop:
|
||||
raise HTTPException(404, "Crop not found")
|
||||
return crop
|
||||
|
||||
|
||||
@router.delete("/{crop_id}", response_model=bool)
|
||||
def delete(crop_id: int, db: Session = Depends(get_db)):
|
||||
crop = crop_service.delete_crop(db, crop_id)
|
||||
if not crop:
|
||||
raise HTTPException(404, "Crop not found")
|
||||
return crop
|
||||
9
routers/file.py
Normal file
9
routers/file.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter, UploadFile, File
|
||||
from service import file_service
|
||||
|
||||
router = APIRouter(prefix="/file", tags=["文件"])
|
||||
|
||||
|
||||
@router.post("/upload", summary="上传文件", response_model=str)
|
||||
async def upload(file: UploadFile = File(..., description="要上传的文件")):
|
||||
return await file_service.upload_file(file)
|
||||
43
routers/patch.py
Normal file
43
routers/patch.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import get_db
|
||||
from schemas.patch import PatchResponse, PatchCreate, PatchUpdate
|
||||
from service import patch_service
|
||||
from service.patch_service import get_patch
|
||||
|
||||
router = APIRouter(prefix="/patches", tags=["patches"])
|
||||
|
||||
|
||||
@router.post("", response_model=bool)
|
||||
def create(data: PatchCreate, db: Session = Depends(get_db)):
|
||||
return patch_service.create_patch(db, data)
|
||||
|
||||
|
||||
@router.get("", response_model=list[PatchResponse])
|
||||
def list_all(db: Session = Depends(get_db)):
|
||||
return patch_service.get_patches(db)
|
||||
|
||||
|
||||
@router.get("/{patch_id}", response_model=PatchResponse)
|
||||
def get_one(patch_id: int, db: Session = Depends(get_db)):
|
||||
patch = get_patch(db, patch_id)
|
||||
if not patch:
|
||||
raise HTTPException(404, "Patch not found")
|
||||
return patch
|
||||
|
||||
|
||||
@router.put("/{patch_id}", response_model=bool)
|
||||
def update(patch_id: int, data: PatchUpdate, db: Session = Depends(get_db)):
|
||||
patch = patch_service.update_patch(db, patch_id, data)
|
||||
if not patch:
|
||||
raise HTTPException(404, "Patch not found")
|
||||
return patch
|
||||
|
||||
|
||||
@router.delete("/{patch_id}", response_model=bool)
|
||||
def delete(patch_id: int, db: Session = Depends(get_db)):
|
||||
patch = patch_service.delete_patch(db, patch_id)
|
||||
if not patch:
|
||||
raise HTTPException(404, "Patch not found")
|
||||
return patch
|
||||
42
routers/record.py
Normal file
42
routers/record.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import get_db
|
||||
from schemas.patch import RecordResponse, RecordCreate, RecordUpdate
|
||||
from service import record_service
|
||||
|
||||
router = APIRouter(prefix="/records", tags=["records"])
|
||||
|
||||
|
||||
@router.post("", response_model=bool)
|
||||
def create(data: RecordCreate, db: Session = Depends(get_db)):
|
||||
return record_service.create_record(db, data)
|
||||
|
||||
|
||||
@router.get("/{record_id}", response_model=RecordResponse)
|
||||
def get_one(record_id: int, db: Session = Depends(get_db)):
|
||||
record = record_service.get_record(db, record_id)
|
||||
if not record:
|
||||
raise HTTPException(404, "Record not found")
|
||||
return record
|
||||
|
||||
|
||||
@router.get("/crop/{crop_id}", response_model=list[RecordResponse])
|
||||
def list_by_crop(crop_id: int, db: Session = Depends(get_db)):
|
||||
return record_service.get_records_by_crop(db, crop_id)
|
||||
|
||||
|
||||
@router.put("/{record_id}", response_model=bool)
|
||||
def update(record_id: int, data: RecordUpdate, db: Session = Depends(get_db)):
|
||||
record = record_service.update_record(db, record_id, data)
|
||||
if not record:
|
||||
raise HTTPException(404, "Record not found")
|
||||
return record
|
||||
|
||||
|
||||
@router.delete("/{record_id}", response_model=bool)
|
||||
def delete(record_id: int, db: Session = Depends(get_db)):
|
||||
record = record_service.delete_record(db, record_id)
|
||||
if not record:
|
||||
raise HTTPException(404, "Record not found")
|
||||
return record
|
||||
92
schemas/patch.py
Normal file
92
schemas/patch.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class PatchCreate(BaseModel):
|
||||
name: str
|
||||
location: Optional[str] = None
|
||||
status: Optional[str] = ""
|
||||
|
||||
|
||||
class PatchUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class CropCreate(BaseModel):
|
||||
patch_id: int
|
||||
name: str
|
||||
variety: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
|
||||
|
||||
class CropUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
variety: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
|
||||
|
||||
class CropResponse(CropCreate):
|
||||
id: int
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
return cls(
|
||||
id=obj.id,
|
||||
patch_id=obj.patch_id,
|
||||
name=obj.name,
|
||||
variety=obj.variety,
|
||||
date=obj.date.strftime("%Y-%m-%d") if obj.date else None,
|
||||
)
|
||||
|
||||
|
||||
class PatchResponse(PatchCreate):
|
||||
id: int
|
||||
crop: Optional[CropResponse] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class RecordCreate(BaseModel):
|
||||
crop_id: int
|
||||
type: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
images: Optional[list[str]] = None
|
||||
videos: Optional[list[str]] = None
|
||||
stage: Optional[str] = None
|
||||
|
||||
|
||||
class RecordUpdate(BaseModel):
|
||||
type: Optional[str] = None
|
||||
date: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
images: Optional[list[str]] = None
|
||||
videos: Optional[list[str]] = None
|
||||
stage: Optional[str] = None
|
||||
|
||||
|
||||
class RecordResponse(RecordCreate):
|
||||
id: int
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
return cls(
|
||||
id=obj.id,
|
||||
crop_id=obj.crop_id,
|
||||
type=obj.type,
|
||||
date=obj.date.strftime("%Y-%m-%d") if obj.date else None,
|
||||
content=obj.content,
|
||||
images=obj.images,
|
||||
videos=obj.videos,
|
||||
stage=obj.stage,
|
||||
)
|
||||
42
service/crop_service.py
Normal file
42
service/crop_service.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.patch import Crops, Records
|
||||
from schemas.patch import CropCreate, CropUpdate
|
||||
|
||||
|
||||
def create_crop(db: Session, data: CropCreate) -> bool:
|
||||
crop = Crops(**data.model_dump())
|
||||
db.add(crop)
|
||||
db.commit()
|
||||
db.refresh(crop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def update_crop(db: Session, crop_id: int, data: CropUpdate) -> bool:
|
||||
result = db.execute(select(Crops).where(Crops.id == crop_id))
|
||||
crop = result.scalar_one_or_none()
|
||||
if not crop:
|
||||
return False
|
||||
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(crop, k, v)
|
||||
db.commit()
|
||||
db.refresh(crop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def delete_crop(db: Session, crop_id: int) -> bool:
|
||||
result = db.execute(select(Crops).where(Crops.id == crop_id))
|
||||
crop = result.scalar_one_or_none()
|
||||
if not crop:
|
||||
return False
|
||||
|
||||
db.execute(delete(Records).where(Records.crop_id == crop_id))
|
||||
|
||||
db.delete(crop)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
50
service/file_service.py
Normal file
50
service/file_service.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import UploadFile, File, HTTPException
|
||||
|
||||
from config.rustfs import s3
|
||||
|
||||
ALLOWED_IMAGE_TYPES = [
|
||||
# 图片
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
# 视频
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/quicktime",
|
||||
"video/x-msvideo",
|
||||
"video/webm",
|
||||
"video/x-matroska",
|
||||
"video/ogg",
|
||||
]
|
||||
|
||||
BUCKET = 'patch'
|
||||
NGINX_PROXY = 'rustfs'
|
||||
|
||||
|
||||
async def upload_file(file: UploadFile = File(...)) -> str:
|
||||
# 校验文件类型
|
||||
if file.content_type not in ALLOWED_IMAGE_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="只允许上传图片或视频文件 (JPEG, PNG, GIF, WEBP, SVG, MP4, MOV, AVI, WEBM, MKV, OGV)"
|
||||
)
|
||||
|
||||
file_ext = file.filename.split('.')[-1]
|
||||
unique_filename = f"{uuid.uuid4().hex}.{file_ext}"
|
||||
|
||||
file_content = await file.read()
|
||||
|
||||
# 上传到S3
|
||||
s3.put_object(
|
||||
Bucket=BUCKET,
|
||||
Key=unique_filename,
|
||||
Body=file_content,
|
||||
ContentType=file.content_type
|
||||
)
|
||||
|
||||
# 返回文件url
|
||||
return f"{NGINX_PROXY}/{BUCKET}/{unique_filename}"
|
||||
79
service/patch_service.py
Normal file
79
service/patch_service.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from models.patch import Patches, Crops, Records
|
||||
from schemas.patch import PatchCreate, PatchUpdate, PatchResponse, CropResponse
|
||||
|
||||
|
||||
def create_patch(db: Session, data: PatchCreate) -> bool:
|
||||
patch = Patches(**data.model_dump())
|
||||
db.add(patch)
|
||||
db.commit()
|
||||
db.refresh(patch)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_patches(db: Session) -> list[PatchResponse]:
|
||||
patches = db.execute(select(Patches)).scalars().all()
|
||||
result = []
|
||||
for p in patches:
|
||||
crop = db.execute(
|
||||
select(Crops).where(Crops.patch_id == p.id)
|
||||
).scalar_one_or_none()
|
||||
data = PatchResponse.model_validate(p)
|
||||
data.crop = CropResponse.from_orm(crop) if crop else None
|
||||
result.append(data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_patch(db: Session, patch_id: int) -> PatchResponse | None:
|
||||
patch = db.execute(
|
||||
select(Patches).where(Patches.id == patch_id)
|
||||
).scalar_one_or_none()
|
||||
if not patch:
|
||||
return None
|
||||
crop = db.execute(
|
||||
select(Crops).where(Crops.patch_id == patch_id)
|
||||
).scalar_one_or_none()
|
||||
data = PatchResponse.model_validate(patch)
|
||||
data.crop = CropResponse.from_orm(crop) if crop else None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def update_patch(db: Session, patch_id: int, data: PatchUpdate) -> bool:
|
||||
result = db.execute(select(Patches).where(Patches.id == patch_id))
|
||||
patch = result.scalar_one_or_none()
|
||||
if not patch:
|
||||
return False
|
||||
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(patch, k, v)
|
||||
db.commit()
|
||||
db.refresh(patch)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def delete_patch(db: Session, patch_id: int) -> bool:
|
||||
result = db.execute(select(Patches).where(Patches.id == patch_id))
|
||||
patch = result.scalar_one_or_none()
|
||||
if not patch:
|
||||
return False
|
||||
|
||||
# 先查作物
|
||||
crop_result = db.execute(select(Crops).where(Crops.patch_id == patch_id))
|
||||
crop = crop_result.scalar_one_or_none()
|
||||
|
||||
# 有作物就先删记录,再删作物
|
||||
if crop:
|
||||
db.execute(delete(Records).where(Records.crop_id == crop.id))
|
||||
db.delete(crop)
|
||||
|
||||
# 最后删菜地
|
||||
db.delete(patch)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
55
service/record_service.py
Normal file
55
service/record_service.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.patch import Records
|
||||
from schemas.patch import RecordCreate, RecordResponse, RecordUpdate
|
||||
|
||||
|
||||
def create_record(db: Session, data: RecordCreate) -> bool:
|
||||
record = Records(**data.model_dump())
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_record(db: Session, record_id: int) -> RecordResponse | None:
|
||||
result = db.execute(select(Records).where(Records.id == record_id))
|
||||
record = result.scalar_one_or_none()
|
||||
|
||||
return RecordResponse.from_orm(record) if record else None
|
||||
|
||||
|
||||
def get_records_by_crop(db: Session, crop_id: int) -> list[RecordResponse]:
|
||||
result = db.execute(
|
||||
select(Records)
|
||||
.where(Records.crop_id == crop_id)
|
||||
.order_by(Records.date.desc())
|
||||
)
|
||||
return [RecordResponse.from_orm(r) for r in result.scalars().all()]
|
||||
|
||||
|
||||
def update_record(db: Session, record_id: int, data: RecordUpdate) -> bool:
|
||||
result = db.execute(select(Records).where(Records.id == record_id))
|
||||
record = result.scalar_one_or_none()
|
||||
if not record:
|
||||
return False
|
||||
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(record, k, v)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def delete_record(db: Session, record_id: int) -> bool:
|
||||
result = db.execute(select(Records).where(Records.id == record_id))
|
||||
record = result.scalar_one_or_none()
|
||||
if record:
|
||||
db.delete(record)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
return False
|
||||
13
utils/common.py
Normal file
13
utils/common.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import re
|
||||
|
||||
|
||||
def camel_to_snake(name: str) -> str:
|
||||
"""将驼峰命名转换为蛇形命名(CamelCase → snake_case)"""
|
||||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
||||
|
||||
|
||||
def snake_to_camel(name: str) -> str:
|
||||
"""将蛇形命名转换为驼峰命名(snake_case → CamelCase)"""
|
||||
components = name.split('_')
|
||||
return ''.join(x.title() for x in components)
|
||||
Reference in New Issue
Block a user