## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
139 lines
4.3 KiB
Python
139 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
生成文章模板与界面占位图资源。
|
|
|
|
项目原先依赖 uniCloud / 七牛的带签名外链图片,签名过期后全部 403,
|
|
导致首页与文章详情顶部出现空白。这里改为生成本地静态资源,
|
|
产物落在 static/template/ 下,无任何外部依赖。
|
|
|
|
用法: python tools/gen-template-assets.py
|
|
"""
|
|
|
|
import os
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
OUT_DIR = os.path.join(ROOT, "static", "template")
|
|
|
|
# 军旅主题配色:(名称, 顶部色, 底部色, 文字色)
|
|
THEMES = [
|
|
("default", (193, 28, 31), (92, 2, 1), (255, 255, 255)),
|
|
("dark", (34, 40, 49), (17, 20, 24), (235, 235, 235)),
|
|
("gold", (176, 122, 40), (110, 71, 16), (255, 250, 235)),
|
|
("olive", (74, 94, 58), (40, 54, 32), (240, 245, 235)),
|
|
("steel", (86, 104, 122), (48, 61, 74), (240, 244, 248)),
|
|
("light", (250, 248, 245), (232, 226, 216), (58, 50, 44)),
|
|
]
|
|
|
|
W, H = 750, 480
|
|
|
|
|
|
def pick_font(size):
|
|
"""挑选一个可用的中文字体,找不到就退回默认字体。"""
|
|
candidates = [
|
|
r"C:\Windows\Fonts\msyh.ttc",
|
|
r"C:\Windows\Fonts\msyhbd.ttc",
|
|
r"C:\Windows\Fonts\simhei.ttf",
|
|
r"C:\Windows\Fonts\simsun.ttc",
|
|
"/System/Library/Fonts/PingFang.ttc",
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
]
|
|
for path in candidates:
|
|
if os.path.exists(path):
|
|
try:
|
|
return ImageFont.truetype(path, size)
|
|
except Exception:
|
|
continue
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def vertical_gradient(size, top, bottom):
|
|
"""竖直线性渐变。"""
|
|
w, h = size
|
|
img = Image.new("RGB", size)
|
|
draw = ImageDraw.Draw(img)
|
|
for y in range(h):
|
|
t = y / max(1, h - 1)
|
|
color = tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3))
|
|
draw.line([(0, y), (w, y)], fill=color)
|
|
return img
|
|
|
|
|
|
def add_decor(img, accent):
|
|
"""右下角加一点几何装饰,避免整块纯色显得像加载失败。"""
|
|
w, h = img.size
|
|
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(overlay)
|
|
for i in range(6):
|
|
radius = 120 + i * 70
|
|
alpha = max(8, 42 - i * 6)
|
|
draw.ellipse(
|
|
[w - radius, h - radius, w + radius * 0.35, h + radius * 0.35],
|
|
outline=(255, 255, 255, alpha),
|
|
width=2,
|
|
)
|
|
draw.rectangle([0, h - 6, w, h], fill=accent + (90,))
|
|
return Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
|
|
|
|
|
|
def build(name, top, bottom, text_color):
|
|
img = vertical_gradient((W, H), top, bottom)
|
|
img = add_decor(img, top)
|
|
|
|
draw = ImageDraw.Draw(img)
|
|
title_font = pick_font(64)
|
|
sub_font = pick_font(28)
|
|
|
|
title = "军歌嘹亮"
|
|
sub = "军旅故事 · 老兵记忆"
|
|
|
|
tw = draw.textlength(title, font=title_font)
|
|
draw.text(((W - tw) / 2, H * 0.36), title, font=title_font, fill=text_color)
|
|
|
|
sw = draw.textlength(sub, font=sub_font)
|
|
draw.text(((W - sw) / 2, H * 0.36 + 92), sub, font=sub_font, fill=text_color)
|
|
|
|
# 分隔线
|
|
line_w = 120
|
|
draw.line(
|
|
[((W - line_w) / 2, H * 0.36 + 66), ((W + line_w) / 2, H * 0.36 + 66)],
|
|
fill=text_color,
|
|
width=2,
|
|
)
|
|
|
|
path = os.path.join(OUT_DIR, f"{name}.png")
|
|
img.save(path, "PNG", optimize=True)
|
|
return path
|
|
|
|
|
|
def build_placeholder_cover():
|
|
"""文章无封面时列表用的占位图。"""
|
|
img = vertical_gradient((W, 420), (243, 241, 238), (226, 221, 214))
|
|
draw = ImageDraw.Draw(img)
|
|
font = pick_font(34)
|
|
text = "暂无封面"
|
|
tw = draw.textlength(text, font=font)
|
|
draw.text(((W - tw) / 2, 190), text, font=font, fill=(160, 152, 144))
|
|
path = os.path.join(OUT_DIR, "no-cover.png")
|
|
img.save(path, "PNG", optimize=True)
|
|
return path
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
made = []
|
|
for name, top, bottom, text_color in THEMES:
|
|
made.append(build(name, top, bottom, text_color))
|
|
made.append(build_placeholder_cover())
|
|
|
|
print(f"输出目录: {OUT_DIR}")
|
|
for p in made:
|
|
size = os.path.getsize(p)
|
|
print(f" {os.path.basename(p):<16} {size/1024:6.1f} KB")
|
|
print(f"共 {len(made)} 个文件")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|