feat: 按环境前缀生成 Apps ID
This commit is contained in:
parent
8747792a4b
commit
de33b3d4b2
@ -67,22 +67,26 @@ DEFAULT_SERVERS = {
|
||||
"测试环境": {
|
||||
"api": "https://api3-dev.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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
}
|
||||
|
||||
# 为已有环境迁移的固定前缀;后续环境从配置中的 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:
|
||||
"""为没有 upload_key 的 app 自动生成,返回是否有变更"""
|
||||
@ -152,7 +234,10 @@ def load_config() -> dict:
|
||||
CONFIG_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(BOOTSTRAP_CONFIG_PATH, CONFIG_JSON_PATH)
|
||||
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:
|
||||
config = json.load(f)
|
||||
# 确保必要字段存在
|
||||
@ -164,8 +249,11 @@ def load_config() -> dict:
|
||||
config["upload"] = DEFAULT_UPLOAD
|
||||
if "versions" not in config:
|
||||
config["versions"] = DEFAULT_VERSIONS.copy()
|
||||
# 自动为缺少 upload_key 的 app 生成唯一标识
|
||||
if _ensure_upload_keys(config):
|
||||
# 自动补齐旧配置的环境前缀、特殊 App 覆盖和 upload_key。
|
||||
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)
|
||||
# 迁移旧式目录皮肤为 ZIP
|
||||
if _migrate_old_themes(config):
|
||||
@ -217,9 +305,9 @@ async def create_app(app: dict):
|
||||
config = load_config()
|
||||
apps = config.get("apps", {})
|
||||
|
||||
# 自动生成 ID
|
||||
numeric_keys = [int(k) for k in apps.keys() if k.isdigit()]
|
||||
new_id = str(max(numeric_keys) + 1) if numeric_keys else "1"
|
||||
# 按服务环境自动生成三位 ID(例如测试环境 1xx、正式环境 2xx)。
|
||||
_apply_special_app_prefix_override(app)
|
||||
new_id = _next_app_id(apps, config.get("servers", {}), app)
|
||||
|
||||
# 自动生成 upload_key
|
||||
if not app.get("upload_key"):
|
||||
@ -558,12 +646,15 @@ async def create_server(server_data: dict):
|
||||
if name in servers:
|
||||
raise HTTPException(status_code=400, detail="环境名称已存在")
|
||||
|
||||
prefix = config.get("next_app_id_prefix", 1)
|
||||
servers[name] = {
|
||||
"api": server_data.get("api", ""),
|
||||
"assDom": server_data.get("assDom", ""),
|
||||
"universalLink": server_data.get("universalLink", ""),
|
||||
"app_id_prefix": prefix,
|
||||
}
|
||||
config["servers"] = servers
|
||||
config["next_app_id_prefix"] = prefix + 1
|
||||
save_config(config)
|
||||
return {"message": "服务器环境已创建"}
|
||||
|
||||
@ -591,6 +682,7 @@ async def update_server(server_name: str, server_data: dict):
|
||||
"api": server_data.get("api", ""),
|
||||
"assDom": server_data.get("assDom", ""),
|
||||
"universalLink": server_data.get("universalLink", ""),
|
||||
"app_id_prefix": servers[new_name].get("app_id_prefix"),
|
||||
}
|
||||
config["servers"] = servers
|
||||
save_config(config)
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
<tr>
|
||||
<th>环境名称</th>
|
||||
<th>API 地址</th>
|
||||
<th>App ID 前缀</th>
|
||||
<th>Associated Domains</th>
|
||||
<th>Universal Link</th>
|
||||
<th>操作</th>
|
||||
@ -75,6 +76,7 @@
|
||||
<tr v-for="(server, name) in servers" :key="name">
|
||||
<td><strong>{{ name }}</strong></td>
|
||||
<td>{{ server.api }}</td>
|
||||
<td>{{ server.app_id_prefix }}</td>
|
||||
<td>{{ server.assDom }}</td>
|
||||
<td>{{ server.universalLink }}</td>
|
||||
<td class="action-btns">
|
||||
@ -273,7 +275,6 @@
|
||||
<input type="text" v-model="versions.app_ver" placeholder="2.180.0">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号</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>
|
||||
|
||||
<template v-if="isAdmin">
|
||||
@ -379,6 +380,11 @@
|
||||
<label>Universal Link</label>
|
||||
<input v-model="serverForm.universalLink" type="text" placeholder="https://dev-data1.readoor.cn">
|
||||
</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;">
|
||||
<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>
|
||||
@ -411,6 +417,10 @@
|
||||
</select>
|
||||
<input v-else v-model="appForm.server" type="text" placeholder="例如:测试环境">
|
||||
</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 class="form-row">
|
||||
<div class="form-group">
|
||||
@ -556,7 +566,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject } from 'vue'
|
||||
import { computed, ref, onMounted, inject } from 'vue'
|
||||
|
||||
const showLogin = inject('showLogin')
|
||||
const getToken = inject('getToken')
|
||||
@ -592,7 +602,13 @@ const jsonContent = ref('{}')
|
||||
|
||||
const showServerModal = ref(false)
|
||||
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 editingAppId = ref(null)
|
||||
@ -729,7 +745,9 @@ const deleteUser = async (u) => {
|
||||
// 服务器环境管理
|
||||
const openServerModal = (name = null, server = null) => {
|
||||
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
|
||||
}
|
||||
|
||||
@ -802,6 +820,7 @@ const openAppModal = (id = null, app = null) => {
|
||||
weixinpay: '',
|
||||
tencent: '',
|
||||
AlivcLicenseKey: '',
|
||||
app_id_prefix_override: '',
|
||||
certificates: {},
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,8 @@ def tmp_config(tmp_path, monkeypatch):
|
||||
"1": {"name": "readoor31", "ossFloder": "test"}
|
||||
},
|
||||
"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"],
|
||||
}))
|
||||
|
||||
@ -20,14 +20,32 @@ def test_get_apps(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.json()["id"] == "2"
|
||||
assert resp.json()["id"] == "100"
|
||||
|
||||
# 验证已创建
|
||||
apps = client.get("/api/config/apps").json()
|
||||
assert "2" in apps
|
||||
assert apps["2"]["name"] == "新App"
|
||||
assert "100" in apps
|
||||
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):
|
||||
@ -121,7 +139,13 @@ def test_create_server(client, tmp_config):
|
||||
"universalLink": "https://new.com",
|
||||
})
|
||||
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):
|
||||
|
||||
@ -54,7 +54,9 @@ def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_conf
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
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
|
||||
versions = client.put("/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers)
|
||||
assert versions.status_code == 200
|
||||
|
||||
Loading…
Reference in New Issue
Block a user