fix: 完善全系统业务闭环(大神入驻联动用户角色、服务评价人工下架监管、友圈搭子上车审核与抽屉)

这个提交包含在:
xcbyy
2026-10-13 18:04:27 +08:00
父节点 c14a1a3474
当前提交 010edc130f
共有 9 个文件被更改,包括 520 次插入 和 12 次删除

查看文件

@@ -55,6 +55,7 @@ export default {
'menu.banner.navCategory': 'Category Navigation',
'menu.community': 'Community & Content',
'menu.community.postManage': 'Post & Comment Audit',
'menu.community.reviewManage': 'Review Management',
'menu.server.dashboard': 'Dashboard-Server',
'menu.server.workplace': 'Workplace-Server',
'menu.server.monitor': 'Monitor-Server',

查看文件

@@ -55,6 +55,7 @@ export default {
'menu.banner.navCategory': '金刚区导航品类',
'menu.community': '友圈与内容管理',
'menu.community.postManage': '动态与评论监管',
'menu.community.reviewManage': '服务评价与回复监管',
'menu.server.dashboard': '仪表盘-服务端',
'menu.server.workplace': '工作台-服务端',
'menu.server.monitor': '实时监控-服务端',

查看文件

@@ -22,6 +22,16 @@ const COMMUNITY: AppRouteRecordRaw = {
roles: ['*'],
},
},
{
path: 'review-manage',
name: 'ReviewManage',
component: () => import('@/views/community/review-manage/index.vue'),
meta: {
locale: 'menu.community.reviewManage',
requiresAuth: true,
roles: ['*'],
},
},
],
};

查看文件

@@ -153,7 +153,7 @@
<a-table-column title="发布时间" data-index="createdAt" :width="160" />
<a-table-column title="管理操作" :width="200" fixed="right">
<a-table-column title="管理操作" :width="280" fixed="right">
<template #cell="{ record }">
<a-space :size="8">
<!-- 置顶切换 -->
@@ -185,6 +185,16 @@
评论
</a-button>
<!-- 搭子上车申请 -->
<a-button
size="small"
type="text"
status="success"
@click="openAppliesDrawer(record)"
>
上车申请
</a-button>
<!-- 违规删除 -->
<a-popconfirm
content="确定彻底删除该条动态及其所有评论吗?此操作不可逆。"
@@ -246,6 +256,62 @@
该动态暂无任何评论互动
</div>
</a-drawer>
<!-- 搭子上车申请抽屉 (Drawer) -->
<a-drawer
v-model:visible="appliesDrawerVisible"
:width="540"
:title="`搭子上车申请 - ${currentPost ? currentPost.author : ''}`"
:footer="false"
>
<div v-if="loadingApplies" style="text-align: center; padding: 40px 0">
<a-spin />
</div>
<div v-else-if="appliesList.length > 0">
<a-list :bordered="false">
<a-list-item v-for="app in appliesList" :key="app.id">
<a-list-item-meta
:title="app.applicantName || app.applicant_name || '搭子玩家'"
:description="`申请时间: ${app.createdAt || app.created_at || '-'} | 用户ID: ${app.applicantId || app.applicant_id || '-'}`"
>
<template #avatar>
<a-avatar :size="36">
<img :src="app.applicantAvatar || app.applicant_avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120'" alt="avatar" />
</a-avatar>
</template>
</a-list-item-meta>
<template #actions>
<a-space>
<a-button
v-if="app.status === 'PENDING' || app.status === '待处理'"
size="mini"
type="primary"
status="success"
@click="handleApplyAction(app.id, 'ACCEPTED')"
>
同意上车
</a-button>
<a-button
v-if="app.status === 'PENDING' || app.status === '待处理'"
size="mini"
type="secondary"
status="danger"
@click="handleApplyAction(app.id, 'REJECTED')"
>
婉拒
</a-button>
<a-tag v-else :color="app.status === 'ACCEPTED' || app.status === '已同意' ? 'green' : 'gray'">
{{ app.status === 'ACCEPTED' ? '已同意' : (app.status === 'REJECTED' ? '已婉拒' : app.status) }}
</a-tag>
</a-space>
</template>
</a-list-item>
</a-list>
</div>
<div v-else style="text-align: center; padding: 60px 0; color: var(--color-text-3)">
该动态暂无任何搭子上车申请
</div>
</a-drawer>
</div>
</template>
@@ -266,6 +332,11 @@
const loadingComments = ref(false);
const commentsList = ref<any[]>([]);
// 搭子上车申请抽屉
const appliesDrawerVisible = ref(false);
const loadingApplies = ref(false);
const appliesList = ref<any[]>([]);
const fetchData = async () => {
loading.value = true;
try {
@@ -345,6 +416,32 @@
}
};
const openAppliesDrawer = async (record: any) => {
currentPost.value = record;
appliesDrawerVisible.value = true;
loadingApplies.value = true;
try {
const res: any = await axios.get(`/api/posts/${record.id}/applies`);
appliesList.value = res.data?.data || res.data || [];
} catch (err) {
appliesList.value = [];
} finally {
loadingApplies.value = false;
}
};
const handleApplyAction = async (applyId: string, status: string) => {
try {
const res: any = await axios.post(`/api/post-applies/${applyId}/handle`, { status });
Message.success(res.data?.message || (status === 'ACCEPTED' ? '已同意上车' : '已婉拒'));
if (currentPost.value) {
openAppliesDrawer(currentPost.value);
}
} catch (err: any) {
Message.error('处置上车申请失败');
}
};
onMounted(() => {
fetchData();
});

查看文件

@@ -0,0 +1,218 @@
<template>
<div class="container">
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item>内容与社区管理</a-breadcrumb-item>
<a-breadcrumb-item>服务评价与回复监管</a-breadcrumb-item>
</a-breadcrumb>
<a-card class="general-card" title="平台订单服务评价与大神回复人工监管中心">
<template #extra>
<a-space>
<a-button @click="fetchData">
<template #icon><icon-refresh /></template>
刷新评价
</a-button>
</a-space>
</template>
<a-alert type="info" style="margin-bottom: 16px">
评价合规底线:平台保护买家客观真实评价权益,同时严厉打击恶意差评敲诈、人身攻击辱骂、同行恶意抹黑或私下引流等违规言论;删除违规评价将自动重新核算大神的综合评分。
</a-alert>
<!-- 搜索筛选栏 -->
<a-row :gutter="16" style="margin-bottom: 16px">
<a-col :span="6">
<a-input
v-model="searchKeyword"
placeholder="搜索评价人、关联订单号或评价内容"
allow-clear
>
<template #prefix><icon-search /></template>
</a-input>
</a-col>
<a-col :span="5">
<a-select v-model="ratingFilter" placeholder="评分星级筛选" allow-clear>
<a-option :value="0">全部星级</a-option>
<a-option :value="5">5星好评</a-option>
<a-option :value="4">4星良好</a-option>
<a-option :value="3">3星中评</a-option>
<a-option :value="2">2星差评</a-option>
<a-option :value="1">1星极差</a-option>
</a-select>
</a-col>
</a-row>
<a-table
:data="filteredList"
:loading="loading"
:pagination="{ pageSize: 10, showTotal: true }"
row-key="id"
>
<template #columns>
<a-table-column title="评价工单ID" data-index="id" :width="140">
<template #cell="{ record }">
<span class="code-font">{{ record.id }}</span>
</template>
</a-table-column>
<a-table-column title="关联订单" data-index="orderId" :width="160">
<template #cell="{ record }">
<span class="code-font">{{ record.orderId }}</span>
</template>
</a-table-column>
<a-table-column title="评价用户" :width="160">
<template #cell="{ record }">
<a-space>
<a-avatar :size="32" shape="circle">
<img :src="record.userAvatar" alt="avatar" />
</a-avatar>
<div>
<div style="font-weight: 600">{{ record.userNickname }}</div>
<div style="font-size: 11px; color: var(--color-text-3)">UID: {{ record.userId }}</div>
</div>
</a-space>
</template>
</a-table-column>
<a-table-column title="被评大神ID" data-index="expertId" :width="120">
<template #cell="{ record }">
<span class="code-font">大神 #{{ record.expertId }}</span>
</template>
</a-table-column>
<a-table-column title="评分与印象标签" :width="200">
<template #cell="{ record }">
<div style="margin-bottom: 4px">
<a-rate :model-value="record.rating" readonly allow-half size="small" />
<span style="font-weight: 600; margin-left: 6px; color: #ff7d00">{{ record.rating }}分</span>
</div>
<a-space wrap :size="4">
<a-tag v-for="(tag, idx) in record.tags" :key="idx" size="small" color="arcoblue">
{{ tag }}
</a-tag>
</a-space>
</template>
</a-table-column>
<a-table-column title="买家评价陈述" :width="280">
<template #cell="{ record }">
<div class="content-box">{{ record.content || '无文字评价' }}</div>
</template>
</a-table-column>
<a-table-column title="大神回复内容" :width="220">
<template #cell="{ record }">
<div v-if="record.replyContent" class="reply-box">
<div style="font-weight: 600; font-size: 11px; color: #165dff">大神回复:</div>
<div>{{ record.replyContent }}</div>
</div>
<span v-else style="color: var(--color-text-4); font-size: 12px">暂未回复</span>
</template>
</a-table-column>
<a-table-column title="评价时间" data-index="createdAt" :width="160" />
<a-table-column title="操作" :width="120" fixed="right" align="center">
<template #cell="{ record }">
<a-popconfirm
content="确定下架并删除该条评价吗?删除后将自动重算大神信誉评级,此操作不可逆。"
type="warning"
@ok="handleDelete(record.id)"
>
<a-button type="text" status="danger" size="small">
违规删除
</a-button>
</a-popconfirm>
</template>
</a-table-column>
</template>
</a-table>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
import { Message } from '@arco-design/web-vue';
const loading = ref(false);
const rawList = ref<any[]>([]);
const searchKeyword = ref('');
const ratingFilter = ref(0);
const fetchData = async () => {
loading.value = true;
try {
const res: any = await axios.get('/api/reviews');
if (res && res.data) {
rawList.value = Array.isArray(res.data) ? res.data : (res.data.data || []);
}
} catch (err: any) {
Message.error('获取评价列表失败');
} finally {
loading.value = false;
}
};
const filteredList = computed(() => {
return rawList.value.filter((item) => {
const matchRating = !ratingFilter.value || Math.floor(item.rating) === ratingFilter.value;
const kw = searchKeyword.value.trim().toLowerCase();
const matchKw =
!kw ||
String(item.id).toLowerCase().includes(kw) ||
String(item.orderId).toLowerCase().includes(kw) ||
String(item.userNickname || '').toLowerCase().includes(kw) ||
String(item.content || '').toLowerCase().includes(kw);
return matchRating && matchKw;
});
});
const handleDelete = async (id: string) => {
try {
const res: any = await axios.delete(`/api/reviews/${id}`);
Message.success(res.data?.message || '违规评价已下架删除');
fetchData();
} catch (err: any) {
Message.error(err.response?.data?.message || err.message || '删除失败');
}
};
onMounted(() => {
fetchData();
});
</script>
<style scoped lang="less">
.container {
padding: 16px 20px;
}
.breadcrumb {
margin-bottom: 16px;
}
.code-font {
font-family: monospace;
font-weight: 600;
}
.content-box {
font-size: 12px;
line-height: 1.5;
color: var(--color-text-1);
background: var(--color-fill-2);
padding: 6px 10px;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.reply-box {
font-size: 12px;
line-height: 1.4;
color: var(--color-text-2);
background: var(--color-fill-3);
padding: 6px 8px;
border-radius: 4px;
border-left: 3px solid #165dff;
}
</style>

查看文件

@@ -84,10 +84,13 @@ export async function initDatabase(): Promise<void> {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 动态为 companions 增加 tags 字段 (若历史表已存在)
// 动态为 companions 增加 tags 字段与 user_id 字段 (若历史表已存在)
try {
await conn.query('ALTER TABLE companions ADD COLUMN tags JSON AFTER id_card_masked');
} catch (_) {}
try {
await conn.query('ALTER TABLE companions ADD COLUMN user_id INT DEFAULT NULL AFTER id');
} catch (_) {}
// 4. 订单表
await conn.query(`
@@ -279,12 +282,21 @@ export async function initDatabase(): Promise<void> {
rating INT NOT NULL DEFAULT 5,
tags JSON,
content TEXT,
reply_content TEXT,
reply_time DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order (order_id),
INDEX idx_expert (expert_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
try {
await conn.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await conn.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
// 14. 友圈动态互动评论表
await conn.query(`
CREATE TABLE IF NOT EXISTS post_comments (

查看文件

@@ -559,6 +559,7 @@ export class MysqlRepository {
avatar?: string;
gender?: string;
}): Promise<any> {
const userId = Number(data.userId) || 1001;
const name = data.name.trim();
const track = data.track || '同城陪吃';
const hourlyRate = Number(data.hourlyRate) || 88;
@@ -569,20 +570,20 @@ export class MysqlRepository {
? data.idCard.substring(0, 6) + '********' + data.idCard.substring(data.idCard.length - 4)
: '110101********0000';
const [existing]: any = await pool.query('SELECT * FROM companions WHERE name = ?', [name]);
const [existing]: any = await pool.query('SELECT * FROM companions WHERE name = ? OR (user_id IS NOT NULL AND user_id = ?)', [name, userId]);
if (existing && existing.length > 0) {
await pool.query(`
UPDATE companions
SET track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', id_card_masked = ?
SET user_id = ?, track = ?, hourly_rate = ?, tags = ?, avatar = ?, gender = ?, audit_status = '待审核', status = '待审核', id_card_masked = ?
WHERE id = ?
`, [track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
`, [userId, track, hourlyRate, tagsJson, avatar, gender, maskedIdCard, existing[0].id]);
return this.getCompanionById(existing[0].id);
}
const [res]: any = await pool.query(`
INSERT INTO companions (name, avatar, gender, age, track, rank_name, hourly_rate, voice_duration, voice_title, rating, order_count, status, audit_status, id_card_masked, tags, apply_time)
VALUES (?, ?, ?, 23, ?, '申请中陪伴师', ?, 15, '热情语音介绍', 5.0, 0, '待审核', '待审核', ?, ?, NOW())
`, [name, avatar, gender, track, hourlyRate, maskedIdCard, tagsJson]);
INSERT INTO companions (user_id, name, avatar, gender, age, track, rank_name, hourly_rate, voice_duration, voice_title, rating, order_count, status, audit_status, id_card_masked, tags, apply_time)
VALUES (?, ?, ?, ?, 23, ?, '申请中陪伴师', ?, 15, '热情语音介绍', 5.0, 0, '待审核', '待审核', ?, ?, NOW())
`, [userId, name, avatar, gender, track, hourlyRate, maskedIdCard, tagsJson]);
return this.getCompanionById(res.insertId);
}
@@ -597,7 +598,21 @@ export class MysqlRepository {
`, [auditStatus, status, id]);
const [rows]: any = await pool.query('SELECT * FROM companions WHERE id = ?', [id]);
return { ...rows[0], message: action === 'approve' ? '资质审核通过' : `已驳回: ${reason}` };
const comp = rows && rows[0];
// 联动更新关联用户的角色与大神状态
if (comp && comp.user_id) {
const uId = Number(comp.user_id);
if (uId > 0) {
if (action === 'approve') {
await pool.query('UPDATE users SET role = "认证大神", is_expert = 1 WHERE id = ?', [uId]);
} else {
await pool.query('UPDATE users SET role = "普通用户", is_expert = 0 WHERE id = ?', [uId]);
}
}
}
return { ...rows[0], message: action === 'approve' ? '资质审核通过,用户已获得大神接单权限' : `已驳回: ${reason}` };
}
// 订单相关
@@ -2603,10 +2618,19 @@ export class MysqlRepository {
}
async getOrderReviews(expertId?: number, orderId?: string): Promise<any[]> {
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
let sql = `
SELECT id, order_id as orderId, expert_id as expertId, user_id as userId,
user_nickname as userNickname, user_avatar as userAvatar,
rating, tags, content, created_at as createdAt
rating, tags, content,
COALESCE(reply_content, '') as replyContent,
reply_time as replyTime,
created_at as createdAt
FROM order_reviews
`;
const params: any[] = [];
@@ -2637,6 +2661,21 @@ export class MysqlRepository {
});
}
async deleteReview(id: string): Promise<boolean> {
const [rows]: any = await pool.query('SELECT expert_id FROM order_reviews WHERE id = ?', [id]);
const expertId = rows && rows[0]?.expert_id;
const [res]: any = await pool.query('DELETE FROM order_reviews WHERE id = ?', [id]);
if (expertId) {
try {
const [avgRows]: any = await pool.query('SELECT AVG(rating) as avgRating FROM order_reviews WHERE expert_id = ?', [expertId]);
const newRating = (avgRows && avgRows[0]?.avgRating) ? Number(Number(avgRows[0].avgRating).toFixed(2)) : 5.0;
await pool.query('UPDATE companions SET rating = ? WHERE id = ?', [newRating, expertId]);
} catch (_) {}
}
return res.affectedRows > 0;
}
// 14. 友圈动态评论
async getPostComments(postId: string): Promise<any[]> {
const [rows]: any = await pool.query(`
@@ -2709,6 +2748,27 @@ export class MysqlRepository {
};
}
async getPostApplies(postId?: string): Promise<any[]> {
let sql = `
SELECT id, post_id as postId, applicant_id as applicantId, applicant_name as applicantName,
applicant_avatar as applicantAvatar, status, created_at as createdAt
FROM post_applies
`;
const params: any[] = [];
if (postId) {
sql += ' WHERE post_id = ?';
params.push(postId);
}
sql += ' ORDER BY created_at DESC';
const [rows]: any = await pool.query(sql, params);
return rows;
}
async handlePostApply(applyId: string, status: 'ACCEPTED' | 'REJECTED'): Promise<any> {
await pool.query('UPDATE post_applies SET status = ? WHERE id = ?', [status, applyId]);
return { id: applyId, status };
}
// 16. 用户个人提现记录查询
async getUserWithdrawals(userId: number): Promise<any[]> {
const [rows]: any = await pool.query(`
@@ -2728,8 +2788,10 @@ export class MysqlRepository {
async replyOrderReview(reviewId: string, replyContent: string): Promise<any> {
const now = new Date();
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN IF NOT EXISTS reply_content TEXT');
await pool.query('ALTER TABLE order_reviews ADD COLUMN IF NOT EXISTS reply_time DATETIME');
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_content TEXT AFTER content');
} catch (_) {}
try {
await pool.query('ALTER TABLE order_reviews ADD COLUMN reply_time DATETIME AFTER reply_content');
} catch (_) {}
await pool.query(`

查看文件

@@ -1903,6 +1903,32 @@ app.post('/api/reviews/:id/reply', async (req: Request, res: Response) => {
}
});
// 管理员全量查询评价与回复列表
app.get('/api/reviews', async (req: Request, res: Response) => {
try {
const expertId = req.query.expertId ? Number(req.query.expertId) : undefined;
const orderId = req.query.orderId ? String(req.query.orderId) : undefined;
const reviews = await repo.getOrderReviews(expertId, orderId);
res.json({
code: 20000,
data: reviews
});
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 管理员违规下架/删除评价
app.delete('/api/reviews/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
await repo.deleteReview(String(id));
res.json({ code: 20000, message: '违规评价已成功删除,大神信誉评分已重新计算' });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 友圈本人动态删除接口
app.delete('/api/posts/:id', async (req: Request, res: Response) => {
try {
@@ -2003,6 +2029,41 @@ app.post('/api/posts/:id/apply', async (req: Request, res: Response) => {
}
});
// 获取指定动态或全部搭子上车申请列表
app.get('/api/posts/:id/applies', async (req: Request, res: Response) => {
try {
const postId = String(req.params.id);
const applies = await repo.getPostApplies(postId);
res.json({ code: 20000, data: applies });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
app.get('/api/post-applies', async (_req: Request, res: Response) => {
try {
const applies = await repo.getPostApplies();
res.json({ code: 20000, data: applies });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 审核/处置搭子上车申请 (同意/谢绝)
app.post('/api/post-applies/:id/handle', async (req: Request, res: Response) => {
try {
const applyId = String(req.params.id);
const { status } = req.body || {};
if (!status || !['ACCEPTED', 'REJECTED'].includes(status)) {
return res.status(400).json({ code: 40000, message: '状态参数必须为 ACCEPTED 或 REJECTED' });
}
const result = await repo.handlePostApply(applyId, status);
res.json({ code: 20000, data: result, message: status === 'ACCEPTED' ? '已同意搭子上车申请' : '已谢绝搭子上车申请' });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 7.8 陪玩收藏管理接口 (收藏/取消收藏/列表/状态探测)
app.get('/api/favorites', async (req: Request, res: Response) => {
try {

查看文件

@@ -326,6 +326,52 @@ describe('ClientFeatures (TDD 测试套件: 用户端功能闭环)', () => {
expect(content).toContain('resetAllFilters');
});
});
describe('9. 大神认证审核双向联动、服务评价监管与友圈搭子上车业务完整闭环', () => {
it('审核大神入驻通过时,必须联动更新 users 表 role 为 "认证大神" 与 is_expert = 1', () => {
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
const content = fs.readFileSync(repoPath, 'utf-8');
expect(content).toContain('auditCompanion');
expect(content).toContain('UPDATE users SET role = "认证大神", is_expert = 1');
expect(content).toContain('UPDATE users SET role = "普通用户", is_expert = 0');
});
it('后端必须提供评价与回复监管接口 (GET /api/reviews 与 DELETE /api/reviews/:id)', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.get('/api/reviews'");
expect(content).toContain("app.delete('/api/reviews/:id'");
});
it('管理后台路由与菜单中必须挂载服务评价与回复监管模块', () => {
const routePath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'router', 'routes', 'modules', 'community.ts');
const content = fs.readFileSync(routePath, 'utf-8');
expect(content).toContain('review-manage');
expect(content).toContain('menu.community.reviewManage');
});
it('后端必须提供搭子上车申请查询与审核处置接口', () => {
const indexPath = path.join(serverSrcDir, 'index.ts');
const content = fs.readFileSync(indexPath, 'utf-8');
expect(content).toContain("app.get('/api/posts/:id/applies'");
expect(content).toContain("app.get('/api/post-applies'");
expect(content).toContain("app.post('/api/post-applies/:id/handle'");
});
it('管理后台动态管理必须支持查看搭子上车申请并执行同意/婉拒处置', () => {
const postManagePath = path.join(rootDir, 'admin', 'arco-design-pro-vite', 'src', 'views', 'community', 'post-manage', 'index.vue');
const content = fs.readFileSync(postManagePath, 'utf-8');
expect(content).toContain('openAppliesDrawer');
expect(content).toContain('appliesDrawerVisible');
expect(content).toContain('handleApplyAction');
expect(content).toContain('/api/post-applies/');
});
});
});