feat: 按环境前缀生成 Apps ID
This commit is contained in:
parent
8747792a4b
commit
de33b3d4b2
@ -67,22 +67,26 @@ DEFAULT_SERVERS = {
|
|||||||
"测试环境": {
|
"测试环境": {
|
||||||
"api": "https://api3-dev.readoor.cn",
|
"api": "https://api3-dev.readoor.cn",
|
||||||
"assDom": "applinks:dev-data1.readoor.cn",
|
"assDom": "applinks:dev-data1.readoor.cn",
|
||||||
"universalLink": "https://dev-data1.readoor.cn"
|
"universalLink": "https://dev-data1.readoor.cn",
|
||||||
|
"app_id_prefix": 1,
|
||||||
},
|
},
|
||||||
"正式环境": {
|
"正式环境": {
|
||||||
"api": "https://api3.readoor.cn",
|
"api": "https://api3.readoor.cn",
|
||||||
"assDom": "applinks:data1.readoor.cn",
|
"assDom": "applinks:data1.readoor.cn",
|
||||||
"universalLink": "https://data1.readoor.cn"
|
"universalLink": "https://data1.readoor.cn",
|
||||||
|
"app_id_prefix": 2,
|
||||||
},
|
},
|
||||||
"华师大环境": {
|
"华师大环境": {
|
||||||
"api": "https://api3.ecnupress.com.cn",
|
"api": "https://api3.ecnupress.com.cn",
|
||||||
"assDom": "applinks:data1.ecnupress.com.cn",
|
"assDom": "applinks:data1.ecnupress.com.cn",
|
||||||
"universalLink": "https://data1.ecnupress.com.cn"
|
"universalLink": "https://data1.ecnupress.com.cn",
|
||||||
|
"app_id_prefix": 3,
|
||||||
},
|
},
|
||||||
"外教环境": {
|
"外教环境": {
|
||||||
"api": "https://weread-api3.sflep.com/api3",
|
"api": "https://weread-api3.sflep.com/api3",
|
||||||
"assDom": "applinks:wereadossda.sflep.com",
|
"assDom": "applinks:wereadossda.sflep.com",
|
||||||
"universalLink": "https://wereadossda.sflep.com"
|
"universalLink": "https://wereadossda.sflep.com",
|
||||||
|
"app_id_prefix": 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +95,84 @@ DEFAULT_VERSIONS = {
|
|||||||
"build_ver": "2.180.0.0",
|
"build_ver": "2.180.0.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 为已有环境迁移的固定前缀;后续环境从配置中的 next_app_id_prefix 自动分配。
|
||||||
|
LEGACY_SERVER_PREFIXES = {
|
||||||
|
"测试环境": 1,
|
||||||
|
"正式环境": 2,
|
||||||
|
"华师大环境": 3,
|
||||||
|
"外教环境": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_server_id_prefixes(config: dict) -> bool:
|
||||||
|
"""为旧配置补齐环境 ID 前缀和下一个可分配前缀。"""
|
||||||
|
changed = False
|
||||||
|
servers = config.get("servers", {})
|
||||||
|
used_prefixes = set()
|
||||||
|
for name, server in servers.items():
|
||||||
|
prefix = server.get("app_id_prefix")
|
||||||
|
if prefix is None and name in LEGACY_SERVER_PREFIXES:
|
||||||
|
prefix = LEGACY_SERVER_PREFIXES[name]
|
||||||
|
server["app_id_prefix"] = prefix
|
||||||
|
changed = True
|
||||||
|
if isinstance(prefix, int) and prefix > 0:
|
||||||
|
used_prefixes.add(prefix)
|
||||||
|
|
||||||
|
next_prefix = max(used_prefixes, default=0) + 1
|
||||||
|
for server in servers.values():
|
||||||
|
prefix = server.get("app_id_prefix")
|
||||||
|
if not isinstance(prefix, int) or prefix <= 0:
|
||||||
|
server["app_id_prefix"] = next_prefix
|
||||||
|
used_prefixes.add(next_prefix)
|
||||||
|
next_prefix += 1
|
||||||
|
changed = True
|
||||||
|
if not isinstance(config.get("next_app_id_prefix"), int) or config["next_app_id_prefix"] < next_prefix:
|
||||||
|
config["next_app_id_prefix"] = next_prefix
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_special_app_prefix_overrides(config: dict) -> bool:
|
||||||
|
"""将英汉大词典的历史 1xx 规则迁移为显式的 App 前缀覆盖。"""
|
||||||
|
changed = False
|
||||||
|
for app in config.get("apps", {}).values():
|
||||||
|
if "英汉大词典" in app.get("name", "") and not app.get("app_id_prefix_override"):
|
||||||
|
app["app_id_prefix_override"] = 1
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_special_app_prefix_override(app: dict) -> None:
|
||||||
|
"""英汉大词典沿用历史 1xx 编号段。"""
|
||||||
|
if "英汉大词典" in app.get("name", "") and not app.get("app_id_prefix_override"):
|
||||||
|
app["app_id_prefix_override"] = 1
|
||||||
|
|
||||||
|
|
||||||
|
def _next_app_id(apps: dict, servers: dict, app: dict) -> str:
|
||||||
|
"""根据环境前缀或 App 特殊覆盖生成下一个配置 ID。"""
|
||||||
|
prefix = app.get("app_id_prefix_override")
|
||||||
|
if prefix in (None, ""):
|
||||||
|
server = servers.get(app.get("server", ""), {})
|
||||||
|
prefix = server.get("app_id_prefix")
|
||||||
|
try:
|
||||||
|
prefix = int(prefix)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
prefix = 0
|
||||||
|
if prefix <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="请选择已配置 Apps ID 规则的服务器环境")
|
||||||
|
|
||||||
|
prefix_text = str(prefix)
|
||||||
|
serials = [
|
||||||
|
int(app_id[len(prefix_text):])
|
||||||
|
for app_id in apps
|
||||||
|
if (app_id.isdigit() and app_id.startswith(prefix_text)
|
||||||
|
and len(app_id) == len(prefix_text) + 2)
|
||||||
|
]
|
||||||
|
next_serial = max(serials, default=-1) + 1
|
||||||
|
if next_serial > 99:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Apps ID 前缀 {prefix} 的编号已用完")
|
||||||
|
return f"{prefix}{next_serial:02d}"
|
||||||
|
|
||||||
|
|
||||||
def _ensure_upload_keys(config: dict) -> bool:
|
def _ensure_upload_keys(config: dict) -> bool:
|
||||||
"""为没有 upload_key 的 app 自动生成,返回是否有变更"""
|
"""为没有 upload_key 的 app 自动生成,返回是否有变更"""
|
||||||
@ -152,7 +234,10 @@ def load_config() -> dict:
|
|||||||
CONFIG_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
|
CONFIG_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy2(BOOTSTRAP_CONFIG_PATH, CONFIG_JSON_PATH)
|
shutil.copy2(BOOTSTRAP_CONFIG_PATH, CONFIG_JSON_PATH)
|
||||||
else:
|
else:
|
||||||
return {"apps": {}, "schemes": {}, "servers": DEFAULT_SERVERS, "branches": ["main"], "upload": DEFAULT_UPLOAD}
|
return {
|
||||||
|
"apps": {}, "schemes": {}, "servers": DEFAULT_SERVERS,
|
||||||
|
"next_app_id_prefix": 5, "branches": ["main"], "upload": DEFAULT_UPLOAD,
|
||||||
|
}
|
||||||
with open(CONFIG_JSON_PATH, "r", encoding="utf-8") as f:
|
with open(CONFIG_JSON_PATH, "r", encoding="utf-8") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
# 确保必要字段存在
|
# 确保必要字段存在
|
||||||
@ -164,8 +249,11 @@ def load_config() -> dict:
|
|||||||
config["upload"] = DEFAULT_UPLOAD
|
config["upload"] = DEFAULT_UPLOAD
|
||||||
if "versions" not in config:
|
if "versions" not in config:
|
||||||
config["versions"] = DEFAULT_VERSIONS.copy()
|
config["versions"] = DEFAULT_VERSIONS.copy()
|
||||||
# 自动为缺少 upload_key 的 app 生成唯一标识
|
# 自动补齐旧配置的环境前缀、特殊 App 覆盖和 upload_key。
|
||||||
if _ensure_upload_keys(config):
|
prefixes_changed = _ensure_server_id_prefixes(config)
|
||||||
|
overrides_changed = _ensure_special_app_prefix_overrides(config)
|
||||||
|
keys_changed = _ensure_upload_keys(config)
|
||||||
|
if prefixes_changed or overrides_changed or keys_changed:
|
||||||
save_config(config)
|
save_config(config)
|
||||||
# 迁移旧式目录皮肤为 ZIP
|
# 迁移旧式目录皮肤为 ZIP
|
||||||
if _migrate_old_themes(config):
|
if _migrate_old_themes(config):
|
||||||
@ -217,9 +305,9 @@ async def create_app(app: dict):
|
|||||||
config = load_config()
|
config = load_config()
|
||||||
apps = config.get("apps", {})
|
apps = config.get("apps", {})
|
||||||
|
|
||||||
# 自动生成 ID
|
# 按服务环境自动生成三位 ID(例如测试环境 1xx、正式环境 2xx)。
|
||||||
numeric_keys = [int(k) for k in apps.keys() if k.isdigit()]
|
_apply_special_app_prefix_override(app)
|
||||||
new_id = str(max(numeric_keys) + 1) if numeric_keys else "1"
|
new_id = _next_app_id(apps, config.get("servers", {}), app)
|
||||||
|
|
||||||
# 自动生成 upload_key
|
# 自动生成 upload_key
|
||||||
if not app.get("upload_key"):
|
if not app.get("upload_key"):
|
||||||
@ -558,12 +646,15 @@ async def create_server(server_data: dict):
|
|||||||
if name in servers:
|
if name in servers:
|
||||||
raise HTTPException(status_code=400, detail="环境名称已存在")
|
raise HTTPException(status_code=400, detail="环境名称已存在")
|
||||||
|
|
||||||
|
prefix = config.get("next_app_id_prefix", 1)
|
||||||
servers[name] = {
|
servers[name] = {
|
||||||
"api": server_data.get("api", ""),
|
"api": server_data.get("api", ""),
|
||||||
"assDom": server_data.get("assDom", ""),
|
"assDom": server_data.get("assDom", ""),
|
||||||
"universalLink": server_data.get("universalLink", ""),
|
"universalLink": server_data.get("universalLink", ""),
|
||||||
|
"app_id_prefix": prefix,
|
||||||
}
|
}
|
||||||
config["servers"] = servers
|
config["servers"] = servers
|
||||||
|
config["next_app_id_prefix"] = prefix + 1
|
||||||
save_config(config)
|
save_config(config)
|
||||||
return {"message": "服务器环境已创建"}
|
return {"message": "服务器环境已创建"}
|
||||||
|
|
||||||
@ -591,6 +682,7 @@ async def update_server(server_name: str, server_data: dict):
|
|||||||
"api": server_data.get("api", ""),
|
"api": server_data.get("api", ""),
|
||||||
"assDom": server_data.get("assDom", ""),
|
"assDom": server_data.get("assDom", ""),
|
||||||
"universalLink": server_data.get("universalLink", ""),
|
"universalLink": server_data.get("universalLink", ""),
|
||||||
|
"app_id_prefix": servers[new_name].get("app_id_prefix"),
|
||||||
}
|
}
|
||||||
config["servers"] = servers
|
config["servers"] = servers
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
|||||||
@ -66,6 +66,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>环境名称</th>
|
<th>环境名称</th>
|
||||||
<th>API 地址</th>
|
<th>API 地址</th>
|
||||||
|
<th>App ID 前缀</th>
|
||||||
<th>Associated Domains</th>
|
<th>Associated Domains</th>
|
||||||
<th>Universal Link</th>
|
<th>Universal Link</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
@ -75,6 +76,7 @@
|
|||||||
<tr v-for="(server, name) in servers" :key="name">
|
<tr v-for="(server, name) in servers" :key="name">
|
||||||
<td><strong>{{ name }}</strong></td>
|
<td><strong>{{ name }}</strong></td>
|
||||||
<td>{{ server.api }}</td>
|
<td>{{ server.api }}</td>
|
||||||
|
<td>{{ server.app_id_prefix }}</td>
|
||||||
<td>{{ server.assDom }}</td>
|
<td>{{ server.assDom }}</td>
|
||||||
<td>{{ server.universalLink }}</td>
|
<td>{{ server.universalLink }}</td>
|
||||||
<td class="action-btns">
|
<td class="action-btns">
|
||||||
@ -273,7 +275,6 @@
|
|||||||
<input type="text" v-model="versions.app_ver" placeholder="2.180.0">
|
<input type="text" v-model="versions.app_ver" placeholder="2.180.0">
|
||||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号</div>
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 12px; color: #999; margin: -8px 0 16px;">保存后 Build_Ver 将自动重置为 App_Ver.0;App Store 打包时构建号自动 +1。</div>
|
|
||||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions">保存版本号</button>
|
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions">保存版本号</button>
|
||||||
|
|
||||||
<template v-if="isAdmin">
|
<template v-if="isAdmin">
|
||||||
@ -379,6 +380,11 @@
|
|||||||
<label>Universal Link</label>
|
<label>Universal Link</label>
|
||||||
<input v-model="serverForm.universalLink" type="text" placeholder="https://dev-data1.readoor.cn">
|
<input v-model="serverForm.universalLink" type="text" placeholder="https://dev-data1.readoor.cn">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>App ID 前缀</label>
|
||||||
|
<input :value="editingServerName ? serverForm.app_id_prefix : nextServerPrefix" type="text" disabled>
|
||||||
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">新增环境自动分配,保存后不可修改。</div>
|
||||||
|
</div>
|
||||||
<div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
|
<div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
|
||||||
<button class="action-btn" style="padding: 8px 16px;" @click="showServerModal = false">取消</button>
|
<button class="action-btn" style="padding: 8px 16px;" @click="showServerModal = false">取消</button>
|
||||||
<button class="btn btn-primary" style="width: auto; padding: 8px 24px;" @click="saveServer">保存</button>
|
<button class="btn btn-primary" style="width: auto; padding: 8px 24px;" @click="saveServer">保存</button>
|
||||||
@ -411,6 +417,10 @@
|
|||||||
</select>
|
</select>
|
||||||
<input v-else v-model="appForm.server" type="text" placeholder="例如:测试环境">
|
<input v-else v-model="appForm.server" type="text" placeholder="例如:测试环境">
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isAdmin" class="form-group">
|
||||||
|
<label>App ID 前缀覆盖</label>
|
||||||
|
<input v-model.number="appForm.app_id_prefix_override" type="number" min="1" placeholder="仅特殊 App 使用">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -556,7 +566,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, inject } from 'vue'
|
import { computed, ref, onMounted, inject } from 'vue'
|
||||||
|
|
||||||
const showLogin = inject('showLogin')
|
const showLogin = inject('showLogin')
|
||||||
const getToken = inject('getToken')
|
const getToken = inject('getToken')
|
||||||
@ -592,7 +602,13 @@ const jsonContent = ref('{}')
|
|||||||
|
|
||||||
const showServerModal = ref(false)
|
const showServerModal = ref(false)
|
||||||
const editingServerName = ref(null)
|
const editingServerName = ref(null)
|
||||||
const serverForm = ref({ name: '', api: '', assDom: '', universalLink: '' })
|
const serverForm = ref({ name: '', api: '', assDom: '', universalLink: '', app_id_prefix: null })
|
||||||
|
const nextServerPrefix = computed(() => {
|
||||||
|
const prefixes = Object.values(servers.value)
|
||||||
|
.map(server => Number(server.app_id_prefix))
|
||||||
|
.filter(prefix => Number.isInteger(prefix) && prefix > 0)
|
||||||
|
return Math.max(0, ...prefixes) + 1
|
||||||
|
})
|
||||||
|
|
||||||
const showAppModal = ref(false)
|
const showAppModal = ref(false)
|
||||||
const editingAppId = ref(null)
|
const editingAppId = ref(null)
|
||||||
@ -729,7 +745,9 @@ const deleteUser = async (u) => {
|
|||||||
// 服务器环境管理
|
// 服务器环境管理
|
||||||
const openServerModal = (name = null, server = null) => {
|
const openServerModal = (name = null, server = null) => {
|
||||||
editingServerName.value = name
|
editingServerName.value = name
|
||||||
serverForm.value = server ? { ...server, name } : { name: '', api: '', assDom: '', universalLink: '' }
|
serverForm.value = server
|
||||||
|
? { ...server, name }
|
||||||
|
: { name: '', api: '', assDom: '', universalLink: '', app_id_prefix: null }
|
||||||
showServerModal.value = true
|
showServerModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -802,6 +820,7 @@ const openAppModal = (id = null, app = null) => {
|
|||||||
weixinpay: '',
|
weixinpay: '',
|
||||||
tencent: '',
|
tencent: '',
|
||||||
AlivcLicenseKey: '',
|
AlivcLicenseKey: '',
|
||||||
|
app_id_prefix_override: '',
|
||||||
certificates: {},
|
certificates: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,8 @@ def tmp_config(tmp_path, monkeypatch):
|
|||||||
"1": {"name": "readoor31", "ossFloder": "test"}
|
"1": {"name": "readoor31", "ossFloder": "test"}
|
||||||
},
|
},
|
||||||
"servers": {
|
"servers": {
|
||||||
"测试环境": {"api": "https://test.api.com", "assDom": "applinks:test.com", "universalLink": "https://test.com"}
|
"测试环境": {"api": "https://test.api.com", "assDom": "applinks:test.com", "universalLink": "https://test.com"},
|
||||||
|
"正式环境": {"api": "https://prod.api.com", "assDom": "applinks:prod.com", "universalLink": "https://prod.com"}
|
||||||
},
|
},
|
||||||
"branches": ["main", "dev"],
|
"branches": ["main", "dev"],
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -20,14 +20,32 @@ def test_get_apps(client, tmp_config):
|
|||||||
|
|
||||||
|
|
||||||
def test_create_app(client, tmp_config):
|
def test_create_app(client, tmp_config):
|
||||||
resp = client.post("/api/config/apps", json={"name": "新App", "AppGuid": "new-guid"})
|
resp = client.post("/api/config/apps", json={
|
||||||
|
"name": "新App", "server": "测试环境", "AppGuid": "new-guid",
|
||||||
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["id"] == "2"
|
assert resp.json()["id"] == "100"
|
||||||
|
|
||||||
# 验证已创建
|
# 验证已创建
|
||||||
apps = client.get("/api/config/apps").json()
|
apps = client.get("/api/config/apps").json()
|
||||||
assert "2" in apps
|
assert "100" in apps
|
||||||
assert apps["2"]["name"] == "新App"
|
assert apps["100"]["name"] == "新App"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_app_uses_environment_prefix_and_dictionary_exception(client, tmp_config):
|
||||||
|
official = client.post("/api/config/apps", json={"name": "正式 App", "server": "正式环境"})
|
||||||
|
dictionary = client.post("/api/config/apps", json={"name": "英汉大词典", "server": "正式环境"})
|
||||||
|
|
||||||
|
assert official.status_code == 200
|
||||||
|
assert official.json()["id"] == "200"
|
||||||
|
assert dictionary.status_code == 200
|
||||||
|
assert dictionary.json()["id"] == "100"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_app_rejects_unknown_environment_id_rule(client, tmp_config):
|
||||||
|
resp = client.post("/api/config/apps", json={"name": "新 App", "server": "未知环境"})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert "ID 规则" in resp.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
def test_update_app(client, tmp_config):
|
def test_update_app(client, tmp_config):
|
||||||
@ -121,7 +139,13 @@ def test_create_server(client, tmp_config):
|
|||||||
"universalLink": "https://new.com",
|
"universalLink": "https://new.com",
|
||||||
})
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert "新环境" in client.get("/api/config/servers").json()
|
servers = client.get("/api/config/servers").json()
|
||||||
|
assert servers["新环境"]["app_id_prefix"] == 3
|
||||||
|
|
||||||
|
client.delete("/api/config/servers/新环境")
|
||||||
|
second = client.post("/api/config/servers", json={"name": "第二环境", "api": "https://second.api.com"})
|
||||||
|
assert second.status_code == 200
|
||||||
|
assert client.get("/api/config/servers").json()["第二环境"]["app_id_prefix"] == 4
|
||||||
|
|
||||||
|
|
||||||
def test_delete_server_in_use(client, tmp_config):
|
def test_delete_server_in_use(client, tmp_config):
|
||||||
|
|||||||
@ -54,7 +54,9 @@ def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_conf
|
|||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
assert client.get("/api/config/apps", headers=headers).status_code == 200
|
assert client.get("/api/config/apps", headers=headers).status_code == 200
|
||||||
assert client.post("/api/config/apps", json={"name": "普通用户 App"}, headers=headers).status_code == 200
|
assert client.post("/api/config/apps", json={
|
||||||
|
"name": "普通用户 App", "server": "测试环境",
|
||||||
|
}, headers=headers).status_code == 200
|
||||||
assert client.get("/api/config/versions", headers=headers).status_code == 200
|
assert client.get("/api/config/versions", headers=headers).status_code == 200
|
||||||
versions = client.put("/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers)
|
versions = client.put("/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers)
|
||||||
assert versions.status_code == 200
|
assert versions.status_code == 200
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user