## 阻断性缺陷 - 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:模板与图标资源生成脚本
148 lines
3.7 KiB
Vue
148 lines
3.7 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="section">
|
||
<view class="section-title">页面跳转自检</view>
|
||
<button class="btn" @click="toHome">首页(switchTab)</button>
|
||
<button class="btn" @click="toSearch">搜索页(switchTab)</button>
|
||
<button class="btn" @click="toDetail">文章详情(需 id)</button>
|
||
<button class="btn" @click="toLogin">登录页</button>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="section-title">云函数连通性自检</view>
|
||
<button class="btn" @click="checkAll" :loading="checking">开始检测</button>
|
||
<button class="btn" @click="clearLog">清空结果</button>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="section-title">结果</view>
|
||
<view class="line" v-for="(item, index) in logs" :key="index">
|
||
<text :class="item.ok ? 'ok' : 'fail'">{{ item.ok ? '✓' : '✗' }}</text>
|
||
<text class="line-text">{{ item.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
/**
|
||
* 自检页:验证页面路由与云函数连通性。
|
||
* 只做只读探测,不写入任何业务数据。
|
||
*/
|
||
export default {
|
||
data() {
|
||
return {
|
||
checking: false,
|
||
logs: []
|
||
}
|
||
},
|
||
methods: {
|
||
log(ok, text) {
|
||
this.logs.unshift({ ok, text });
|
||
},
|
||
clearLog() {
|
||
this.logs = [];
|
||
},
|
||
|
||
// ── 路由自检 ──
|
||
toHome() {
|
||
uni.switchTab({ url: '/uni_modules/uni-cms-article/pages/list/list' });
|
||
},
|
||
toSearch() {
|
||
uni.switchTab({ url: '/uni_modules/uni-cms-article/pages/list/slist' });
|
||
},
|
||
toDetail() {
|
||
uni.navigateTo({ url: '/uni_modules/uni-cms-article/pages/detail/detail' });
|
||
},
|
||
toLogin() {
|
||
uni.navigateTo({
|
||
url: '/pages3/uni_modules/uni-id-pages/pages/login/login-withoutpwd'
|
||
});
|
||
},
|
||
|
||
// ── 云函数自检 ──
|
||
async checkAll() {
|
||
this.checking = true;
|
||
this.logs = [];
|
||
try {
|
||
await this.checkCloudFn('uni-cms-categories', 'getTags', {});
|
||
await this.checkCloudFn('gift_info', 'getGiftList', {});
|
||
await this.checkCloudFn('comments', 'getComments', { article_id: '' });
|
||
await this.checkCloudFn('cms-vote', 'getVoteInfo', { vote_id: '' });
|
||
await this.checkCloudFn('cms-articles-log', 'getHistoryList', {});
|
||
this.log(uni.getStorageSync('uni_id_token') ? true : false,
|
||
uni.getStorageSync('uni_id_token') ? '登录态:已登录' : '登录态:未登录(部分接口会拒绝,属正常)');
|
||
} finally {
|
||
this.checking = false;
|
||
}
|
||
},
|
||
async checkCloudFn(name, method, params) {
|
||
try {
|
||
const obj = uniCloud.importObject(name, { customUI: true });
|
||
if (typeof obj[method] !== 'function') {
|
||
this.log(false, `${name}.${method}() 不存在`);
|
||
return;
|
||
}
|
||
const res = await obj[method](params);
|
||
const code = res && (res.code !== undefined ? res.code : res.errCode);
|
||
const ok = code === 200 || code === '200' || code === 0 || code === '0';
|
||
this.log(ok, `${name}.${method}() → ${ok ? '正常' : `返回 ${JSON.stringify(code)}`}`);
|
||
} catch (e) {
|
||
this.log(false, `${name}.${method}() → ${e.errMsg || e.message || '调用失败'}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.page {
|
||
padding: 24rpx;
|
||
background-color: #f7f7f7;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.section {
|
||
background-color: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.btn {
|
||
font-size: 28rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.line {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
padding: 8rpx 0;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.ok {
|
||
color: #18b566;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.fail {
|
||
color: #dd524d;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.line-text {
|
||
flex: 1;
|
||
color: #555;
|
||
word-break: break-all;
|
||
}
|
||
</style>
|