fix: NUXT_PUBLIC_APP_ID 改为数字主键,接口基址切正式域名

1. appId 语义修正
   NUXT_PUBLIC_APP_ID 之前填的是 productCode('website'),但下游拿它去请求
   /api/app/product/detail/{productId},后端按 Integer 解析主键,于是转换失败。
   改为 app_product.product_id 数字主键(种子数据为 42);nuxt.config.ts 的
   缺省值同步去掉 'website' 回退——留空时下游按空值降级,绝不回退到编码值。

2. 接口基址切 hjc-api.sitelnk.cn
   .env.example / .env.production / nuxt.config.ts 的缺省值与 preconnect、
   dns-prefetch 一并从 hjc-api.websoft.top 改为正式域名;scripts/ 与
   outputs/ 文档里的旧地址同步更新。

注:本提交含若干文件的 CRLF/LF 行尾归一,实际内容改动见 git diff --ignore-cr-at-eol。
This commit is contained in:
2026-09-25 05:22:58 +08:00
parent 3d3a377221
commit 4a5a8f306a
12 changed files with 582 additions and 564 deletions
+98 -98
View File
@@ -1,98 +1,98 @@
#!/usr/bin/env python3
"""批量:上传本地图到文件服务,并把完整URL写回CMS记录的 image 字段。
用法: python3 cms_fill_image.py <mapping.json> <result.json>
mapping.json: [{"module":"cms-article","id":10570,"file":"/path/to/img.png"}, ...]
"""
import os, json, sys, subprocess, time, urllib.request, urllib.error
TOKEN = os.environ.get("TOKEN", "")
UPLOAD = "https://server.websoft.top/api/file/upload"
CMS = "https://hjc-api.websoft.top/api"
FILE_BASE = "https://file.websoft.top/api/file/"
HDR = {"TenantId": "10626", "Authorization": "Bearer " + TOKEN}
def _retry(fn, tries=4, sleep=2):
last = None
for i in range(tries):
try:
return fn()
except Exception as e:
last = e
time.sleep(sleep * (i + 1))
raise last
def upload(local_path: str) -> str:
def _do():
out = subprocess.run(
["curl", "-s", "-m", "60", "-X", "POST", UPLOAD,
"-H", "TenantId: 10626", "-H", f"Authorization: Bearer {TOKEN}",
"-F", f"file=@{local_path}"],
capture_output=True, text=True,
)
try:
d = json.loads(out.stdout)
except Exception:
raise RuntimeError(f"upload non-json: {out.stdout[:200]}")
if d.get("code") != 0:
raise RuntimeError(f"upload code={d.get('code')} msg={d.get('message')} err={str(d.get('error'))[:120]}")
p = (d.get("data") or {}).get("path", "").lstrip("/")
if not p:
raise RuntimeError("upload returned empty path")
return FILE_BASE + p
return _retry(_do)
def get_item(module: str, id_: int):
def _do():
url = f"{CMS}/cms/{module}/{id_}"
req = urllib.request.Request(url, headers=HDR)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())["data"]
except urllib.error.HTTPError as e:
body = e.read()[:300]
raise RuntimeError(f"GET {url} -> HTTP {e.code}: {body}")
return _retry(_do)
def put_item(module: str, obj: dict):
def _do():
url = f"{CMS}/cms/{module}"
req = urllib.request.Request(
url, data=json.dumps(obj).encode(),
headers={**HDR, "Content-Type": "application/json"}, method="PUT",
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
return _retry(_do)
def main():
mapping = json.load(open(sys.argv[1]))
result_path = sys.argv[2] if len(sys.argv) > 2 else "/tmp/fill_result.json"
results = []
for it in mapping:
mod, iid, fpath = it["module"], it["id"], it["file"]
rec = {"module": mod, "id": iid}
try:
url = upload(fpath)
obj = get_item(mod, iid)
obj["image"] = url
out = put_item(mod, obj)
rec["url"] = url
rec["put_code"] = out.get("code")
rec["ok"] = out.get("code") == 0
print(f"[{mod} {iid}] upload OK -> PUT code={out.get('code')} {'OK' if rec['ok'] else 'FAIL'} | {url[:70]}")
except Exception as e:
rec["error"] = str(e)[:300]
print(f"[{mod} {iid}] ERROR: {e}")
results.append(rec)
time.sleep(1)
json.dump(results, open(result_path, "w"), ensure_ascii=False, indent=2)
print(f"\n=== done {len(results)} items, saved {result_path} ===")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""批量:上传本地图到文件服务,并把完整URL写回CMS记录的 image 字段。
用法: python3 cms_fill_image.py <mapping.json> <result.json>
mapping.json: [{"module":"cms-article","id":10570,"file":"/path/to/img.png"}, ...]
"""
import os, json, sys, subprocess, time, urllib.request, urllib.error
TOKEN = os.environ.get("TOKEN", "")
UPLOAD = "https://server.websoft.top/api/file/upload"
CMS = "https://hjc-api.sitelnk.cn/api"
FILE_BASE = "https://file.websoft.top/api/file/"
HDR = {"TenantId": "10626", "Authorization": "Bearer " + TOKEN}
def _retry(fn, tries=4, sleep=2):
last = None
for i in range(tries):
try:
return fn()
except Exception as e:
last = e
time.sleep(sleep * (i + 1))
raise last
def upload(local_path: str) -> str:
def _do():
out = subprocess.run(
["curl", "-s", "-m", "60", "-X", "POST", UPLOAD,
"-H", "TenantId: 10626", "-H", f"Authorization: Bearer {TOKEN}",
"-F", f"file=@{local_path}"],
capture_output=True, text=True,
)
try:
d = json.loads(out.stdout)
except Exception:
raise RuntimeError(f"upload non-json: {out.stdout[:200]}")
if d.get("code") != 0:
raise RuntimeError(f"upload code={d.get('code')} msg={d.get('message')} err={str(d.get('error'))[:120]}")
p = (d.get("data") or {}).get("path", "").lstrip("/")
if not p:
raise RuntimeError("upload returned empty path")
return FILE_BASE + p
return _retry(_do)
def get_item(module: str, id_: int):
def _do():
url = f"{CMS}/cms/{module}/{id_}"
req = urllib.request.Request(url, headers=HDR)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())["data"]
except urllib.error.HTTPError as e:
body = e.read()[:300]
raise RuntimeError(f"GET {url} -> HTTP {e.code}: {body}")
return _retry(_do)
def put_item(module: str, obj: dict):
def _do():
url = f"{CMS}/cms/{module}"
req = urllib.request.Request(
url, data=json.dumps(obj).encode(),
headers={**HDR, "Content-Type": "application/json"}, method="PUT",
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
return _retry(_do)
def main():
mapping = json.load(open(sys.argv[1]))
result_path = sys.argv[2] if len(sys.argv) > 2 else "/tmp/fill_result.json"
results = []
for it in mapping:
mod, iid, fpath = it["module"], it["id"], it["file"]
rec = {"module": mod, "id": iid}
try:
url = upload(fpath)
obj = get_item(mod, iid)
obj["image"] = url
out = put_item(mod, obj)
rec["url"] = url
rec["put_code"] = out.get("code")
rec["ok"] = out.get("code") == 0
print(f"[{mod} {iid}] upload OK -> PUT code={out.get('code')} {'OK' if rec['ok'] else 'FAIL'} | {url[:70]}")
except Exception as e:
rec["error"] = str(e)[:300]
print(f"[{mod} {iid}] ERROR: {e}")
results.append(rec)
time.sleep(1)
json.dump(results, open(result_path, "w"), ensure_ascii=False, indent=2)
print(f"\n=== done {len(results)} items, saved {result_path} ===")
if __name__ == "__main__":
main()
+141 -141
View File
@@ -1,141 +1,141 @@
#!/usr/bin/env node
/**
* CMS 内容批量发布脚本
* ============================================================================
* 背景(2026-08-05):
* website-admin 后台的状态枚举与 CMS 后端语义相反——后台点「发布 / 上架」写入
* status=1,而 CMS 权威语义是 0=已发布、1=待审核、2=已下线,
* C 端 website-template(server/api/{article,product,case}/list.get.ts)只查
* status=0,于是后台明明「已发布」,官网却一条都不显示。
*
* 后台枚举已在本次一并修正,但存量数据仍停在 status=1,需要用本脚本批量刷成 0。
*
* 用法:
* # 1) 先空跑,只打印将要修改的内容,不写任何数据
* ADMIN_TOKEN="eyJ..." TENANT_ID=10546 node scripts/publish-cms-content.mjs
*
* # 2) 确认无误后真正执行
* ADMIN_TOKEN="eyJ..." TENANT_ID=10546 APPLY=1 node scripts/publish-cms-content.mjs
*
* # 只处理文章
* ADMIN_TOKEN="eyJ..." TENANT_ID=10546 MODULES=article APPLY=1 node scripts/publish-cms-content.mjs
*
* token 获取:后台 DevTools → Network → 任意 XHR → Request Headers → Authorization
* (连同 "Bearer " 前缀一起复制即可,脚本会自动补全)
* ============================================================================
*/
const API_BASE = process.env.CMS_API_BASE || 'https://hjc-api.websoft.top/api'
const TOKEN_RAW = process.env.ADMIN_TOKEN || ''
const TENANT_ID = process.env.TENANT_ID || '10546'
const APPLY = process.env.APPLY === '1'
const MODULES = (process.env.MODULES || 'article,product,case')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
/** CMS 权威语义:0=已发布 / 在售(C 端可见) */
const PUBLISHED = 0
/** 各模块的接口路径与主键字段 */
const MODULE_CONF = {
article: { path: 'cms-article', idKey: 'articleId', titleKey: 'title' },
product: { path: 'cms-product', idKey: 'productId', titleKey: 'productName' },
case: { path: 'cms-case', idKey: 'caseId', titleKey: 'title' }
}
if (!TOKEN_RAW) {
console.error('❌ 缺少 ADMIN_TOKEN。')
console.error(' ADMIN_TOKEN="eyJ..." TENANT_ID=10546 node scripts/publish-cms-content.mjs')
process.exit(1)
}
const TOKEN = TOKEN_RAW.startsWith('Bearer ') ? TOKEN_RAW : `Bearer ${TOKEN_RAW}`
const headers = {
'Content-Type': 'application/json',
Authorization: TOKEN,
TenantId: String(TENANT_ID)
}
async function fetchList(conf) {
const url = `${API_BASE}/cms/${conf.path}/page?page=1&limit=200`
const res = await fetch(url, { headers })
const json = await res.json().catch(() => null)
if (!json || json.code !== 0) {
throw new Error(`拉取列表失败 (${res.status}): ${JSON.stringify(json)?.slice(0, 200)}`)
}
return json.data?.list || []
}
async function updateStatus(conf, id) {
const url = `${API_BASE}/cms/${conf.path}/status`
const res = await fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify({ [conf.idKey]: id, status: PUBLISHED })
})
const text = await res.text()
let json = null
try { json = JSON.parse(text) } catch { /* 非 JSON 响应 */ }
return { ok: res.ok && (json === null || json.code === 0), status: res.status, body: text.slice(0, 200) }
}
async function run() {
console.log(`租户 ${TENANT_ID} | 模块 ${MODULES.join(', ')} | 模式 ${APPLY ? '实际执行' : '空跑预演'}`)
console.log('─'.repeat(70))
let totalPending = 0
let totalOk = 0
let totalFail = 0
for (const mod of MODULES) {
const conf = MODULE_CONF[mod]
if (!conf) {
console.log(`跳过未知模块 ${mod}`)
continue
}
let list
try {
list = await fetchList(conf)
} catch (e) {
console.log(`\n[${mod}] 拉取失败:${e.message}`)
continue
}
const pending = list.filter((it) => Number(it.status) !== PUBLISHED)
console.log(`\n[${mod}] 共 ${list.length} 条,其中 ${pending.length} 条未发布(status !== 0)`)
totalPending += pending.length
for (const it of pending) {
const id = it[conf.idKey] ?? it.id
const title = String(it[conf.titleKey] || it.title || '(无标题)').slice(0, 30)
if (!APPLY) {
console.log(` · [预演] ${id} status=${it.status} → 0 | ${title}`)
continue
}
const r = await updateStatus(conf, id)
if (r.ok) {
totalOk++
console.log(` ✓ ${id} → 已发布 | ${title}`)
} else {
totalFail++
console.log(` ✗ ${id} 失败 HTTP ${r.status} ${r.body} | ${title}`)
}
}
}
console.log('\n' + '─'.repeat(70))
if (APPLY) {
console.log(`完成:成功 ${totalOk} 条,失败 ${totalFail} 条`)
console.log('前台若有缓存,等待缓存过期或重启 Node 进程后即可看到内容。')
} else {
console.log(`空跑结束:共 ${totalPending} 条待发布。加 APPLY=1 重新运行即可实际写入。`)
}
}
run().catch((e) => {
console.error('执行出错:', e)
process.exit(1)
})
#!/usr/bin/env node
/**
* CMS 内容批量发布脚本
* ============================================================================
* 背景(2026-08-05):
* website-admin 后台的状态枚举与 CMS 后端语义相反——后台点「发布 / 上架」写入
* status=1,而 CMS 权威语义是 0=已发布、1=待审核、2=已下线,
* C 端 website-template(server/api/{article,product,case}/list.get.ts)只查
* status=0,于是后台明明「已发布」,官网却一条都不显示。
*
* 后台枚举已在本次一并修正,但存量数据仍停在 status=1,需要用本脚本批量刷成 0。
*
* 用法:
* # 1) 先空跑,只打印将要修改的内容,不写任何数据
* ADMIN_TOKEN="eyJ..." TENANT_ID=10546 node scripts/publish-cms-content.mjs
*
* # 2) 确认无误后真正执行
* ADMIN_TOKEN="eyJ..." TENANT_ID=10546 APPLY=1 node scripts/publish-cms-content.mjs
*
* # 只处理文章
* ADMIN_TOKEN="eyJ..." TENANT_ID=10546 MODULES=article APPLY=1 node scripts/publish-cms-content.mjs
*
* token 获取:后台 DevTools → Network → 任意 XHR → Request Headers → Authorization
* (连同 "Bearer " 前缀一起复制即可,脚本会自动补全)
* ============================================================================
*/
const API_BASE = process.env.CMS_API_BASE || 'https://hjc-api.sitelnk.cn/api'
const TOKEN_RAW = process.env.ADMIN_TOKEN || ''
const TENANT_ID = process.env.TENANT_ID || '10546'
const APPLY = process.env.APPLY === '1'
const MODULES = (process.env.MODULES || 'article,product,case')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
/** CMS 权威语义:0=已发布 / 在售(C 端可见) */
const PUBLISHED = 0
/** 各模块的接口路径与主键字段 */
const MODULE_CONF = {
article: { path: 'cms-article', idKey: 'articleId', titleKey: 'title' },
product: { path: 'cms-product', idKey: 'productId', titleKey: 'productName' },
case: { path: 'cms-case', idKey: 'caseId', titleKey: 'title' }
}
if (!TOKEN_RAW) {
console.error('❌ 缺少 ADMIN_TOKEN。')
console.error(' ADMIN_TOKEN="eyJ..." TENANT_ID=10546 node scripts/publish-cms-content.mjs')
process.exit(1)
}
const TOKEN = TOKEN_RAW.startsWith('Bearer ') ? TOKEN_RAW : `Bearer ${TOKEN_RAW}`
const headers = {
'Content-Type': 'application/json',
Authorization: TOKEN,
TenantId: String(TENANT_ID)
}
async function fetchList(conf) {
const url = `${API_BASE}/cms/${conf.path}/page?page=1&limit=200`
const res = await fetch(url, { headers })
const json = await res.json().catch(() => null)
if (!json || json.code !== 0) {
throw new Error(`拉取列表失败 (${res.status}): ${JSON.stringify(json)?.slice(0, 200)}`)
}
return json.data?.list || []
}
async function updateStatus(conf, id) {
const url = `${API_BASE}/cms/${conf.path}/status`
const res = await fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify({ [conf.idKey]: id, status: PUBLISHED })
})
const text = await res.text()
let json = null
try { json = JSON.parse(text) } catch { /* 非 JSON 响应 */ }
return { ok: res.ok && (json === null || json.code === 0), status: res.status, body: text.slice(0, 200) }
}
async function run() {
console.log(`租户 ${TENANT_ID} | 模块 ${MODULES.join(', ')} | 模式 ${APPLY ? '实际执行' : '空跑预演'}`)
console.log('─'.repeat(70))
let totalPending = 0
let totalOk = 0
let totalFail = 0
for (const mod of MODULES) {
const conf = MODULE_CONF[mod]
if (!conf) {
console.log(`跳过未知模块 ${mod}`)
continue
}
let list
try {
list = await fetchList(conf)
} catch (e) {
console.log(`\n[${mod}] 拉取失败:${e.message}`)
continue
}
const pending = list.filter((it) => Number(it.status) !== PUBLISHED)
console.log(`\n[${mod}] 共 ${list.length} 条,其中 ${pending.length} 条未发布(status !== 0)`)
totalPending += pending.length
for (const it of pending) {
const id = it[conf.idKey] ?? it.id
const title = String(it[conf.titleKey] || it.title || '(无标题)').slice(0, 30)
if (!APPLY) {
console.log(` · [预演] ${id} status=${it.status} → 0 | ${title}`)
continue
}
const r = await updateStatus(conf, id)
if (r.ok) {
totalOk++
console.log(` ✓ ${id} → 已发布 | ${title}`)
} else {
totalFail++
console.log(` ✗ ${id} 失败 HTTP ${r.status} ${r.body} | ${title}`)
}
}
}
console.log('\n' + '─'.repeat(70))
if (APPLY) {
console.log(`完成:成功 ${totalOk} 条,失败 ${totalFail} 条`)
console.log('前台若有缓存,等待缓存过期或重启 Node 进程后即可看到内容。')
} else {
console.log(`空跑结束:共 ${totalPending} 条待发布。加 APPLY=1 重新运行即可实际写入。`)
}
}
run().catch((e) => {
console.error('执行出错:', e)
process.exit(1)
})