feat(withdraw): build dedicated withdrawal page with optimized layout and multichannel support
这个提交包含在:
@@ -72,6 +72,36 @@ export const ICONS = {
|
||||
</svg>
|
||||
`),
|
||||
|
||||
chevronLeft: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A3B8" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
alipay: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#0284C7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="4"></rect>
|
||||
<path d="M7 9h10"></path>
|
||||
<path d="M12 9v4"></path>
|
||||
<path d="M8 15c2 0 4-1 6-4"></path>
|
||||
<path d="M11 13c1 2 3 4 5 4"></path>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
bankCard: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2"></rect>
|
||||
<line x1="2" y1="10" x2="22" y2="10"></line>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
checkCircle: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="9 12 11 14 15 10"></polyline>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
starFilled: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#F59E0B">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||||
|
||||
@@ -112,6 +112,16 @@
|
||||
"backgroundColor": "#0A0F1D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/withdraw",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "收益提现",
|
||||
"navigationBarBackgroundColor": "#0A0F1D",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#0A0F1D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/create",
|
||||
"style": {
|
||||
|
||||
@@ -168,12 +168,70 @@
|
||||
<image class="action-svg" :src="ICONS.chat" mode="aspectFit" />
|
||||
<text class="action-text">私聊</text>
|
||||
</view>
|
||||
<view class="icon-action btn-active" @click="openReportModal">
|
||||
<image class="action-svg" :src="ICONS.shield" mode="aspectFit" />
|
||||
<text class="action-text">举报</text>
|
||||
</view>
|
||||
<view class="price-summary">
|
||||
<text class="summary-label">当前选定:</text>
|
||||
<text class="summary-price">¥{{ currentSelectedService ? currentSelectedService.price : expert.price }}</text>
|
||||
</view>
|
||||
<button class="btn-book btn-active" @click="goToOrder">立即预约</button>
|
||||
</view>
|
||||
|
||||
<!-- 举报不良行为弹窗 -->
|
||||
<view class="review-modal-mask" v-if="reportModalVisible" @click.self="closeReportModal">
|
||||
<view class="review-modal-content">
|
||||
<view class="modal-header">
|
||||
<view class="modal-header-left">
|
||||
<text class="modal-title">举报违规行为</text>
|
||||
<text class="modal-subtitle">
|
||||
举报对象:{{ expert.nickname }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="modal-close" @click="closeReportModal">
|
||||
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 违规类型单选 -->
|
||||
<view class="tags-section">
|
||||
<text class="tags-label">选择违规类型</text>
|
||||
<view class="tags-wrap">
|
||||
<view
|
||||
v-for="item in presetReportReasons"
|
||||
:key="item"
|
||||
class="review-tag"
|
||||
:class="{ active: reportForm.reason === item }"
|
||||
@click="reportForm.reason = item"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 举报描述 -->
|
||||
<view class="textarea-section">
|
||||
<text class="textarea-label">详细违规线索说明</text>
|
||||
<textarea
|
||||
class="review-textarea"
|
||||
v-model="reportForm.description"
|
||||
placeholder="请填写具体违规线索(如涉黄言论、私下诱导转账、脱离平台等),平台将严肃核处..."
|
||||
placeholder-class="textarea-placeholder"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="char-count">{{ reportForm.description.length }}/200</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="modal-actions">
|
||||
<button class="modal-btn cancel-btn btn-active" @click="closeReportModal">取消</button>
|
||||
<button class="modal-btn submit-btn btn-active danger" :disabled="submittingReport" @click="submitReportTicket">
|
||||
{{ submittingReport ? '正在提交...' : '确认提交举报' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -181,7 +239,7 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import SkeletonScreen from '@/components/SkeletonScreen.vue';
|
||||
import { getCompanionDetail, getExpertReviews, replyOrderReview } from '@/common/api.js';
|
||||
import { getCompanionDetail, getExpertReviews, replyOrderReview, submitReport as submitReportApi } from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
|
||||
const isLoading = ref(true);
|
||||
@@ -325,6 +383,57 @@ const goToOrder = () => {
|
||||
url: `/pages/order/create?expertId=${expert.value.id}&serviceId=${srv.id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 举报违规行为状态与提交
|
||||
const reportModalVisible = ref(false);
|
||||
const submittingReport = ref(false);
|
||||
const presetReportReasons = [
|
||||
'涉黄低俗言论',
|
||||
'私下诱导转账/欺诈',
|
||||
'态度恶劣/人身攻击',
|
||||
'开挂/作弊/战绩造假',
|
||||
'线下违规/安全隐患',
|
||||
'其他违规行为'
|
||||
];
|
||||
const reportForm = ref({
|
||||
reason: '涉黄低俗言论',
|
||||
description: ''
|
||||
});
|
||||
|
||||
const openReportModal = () => {
|
||||
reportForm.value = {
|
||||
reason: '涉黄低俗言论',
|
||||
description: ''
|
||||
};
|
||||
reportModalVisible.value = true;
|
||||
};
|
||||
|
||||
const closeReportModal = () => {
|
||||
reportModalVisible.value = false;
|
||||
};
|
||||
|
||||
const submitReportTicket = async () => {
|
||||
submittingReport.value = true;
|
||||
uni.showLoading({ title: '正在提交举报...' });
|
||||
try {
|
||||
await submitReportApi({
|
||||
reporterId: 1001,
|
||||
targetType: 'EXPERT',
|
||||
targetId: String(expert.value.id || '101'),
|
||||
targetName: expert.value.nickname || '同城陪伴师',
|
||||
reason: reportForm.value.reason,
|
||||
description: reportForm.value.description.trim()
|
||||
});
|
||||
uni.hideLoading();
|
||||
closeReportModal();
|
||||
uni.showToast({ title: '举报已受理,平台将在24小时内核实处置', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: err.message || '举报提交失败', icon: 'none' });
|
||||
} finally {
|
||||
submittingReport.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -870,4 +979,165 @@ const goToOrder = () => {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.review-modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.review-modal-content {
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
background: #151D30;
|
||||
border-top-left-radius: 36rpx;
|
||||
border-top-right-radius: 36rpx;
|
||||
padding: 36rpx 36rpx calc(36rpx + env(safe-area-inset-bottom));
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: none;
|
||||
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.5);
|
||||
animation: slideUp 0.25s ease-out;
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.modal-header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
|
||||
.modal-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
.modal-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.close-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-section {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.tags-label {
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tags-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
.review-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 30rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
color: #FFFFFF;
|
||||
background: rgba(239, 68, 68, 0.25);
|
||||
border-color: #EF4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-section {
|
||||
margin-bottom: 32rpx;
|
||||
position: relative;
|
||||
|
||||
.textarea-label {
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.review-textarea {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
box-sizing: border-box;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
bottom: 16rpx;
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
|
||||
&.cancel-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
&.submit-btn {
|
||||
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 16rpx rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
<text class="sec-title">陪玩服务管理 ({{ expertServices.length }})</text>
|
||||
</view>
|
||||
<view class="service-actions">
|
||||
<button class="btn-wd-records btn-active" @click="goToWithdrawPage">收益提现</button>
|
||||
<button class="btn-wd-records btn-active" @click="openWithdrawRecords">提现记录</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -505,6 +506,12 @@ const toggleServiceStatus = async (item) => {
|
||||
const showWithdrawRecordsModal = ref(false);
|
||||
const withdrawRecords = ref([]);
|
||||
|
||||
const goToWithdrawPage = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/withdraw'
|
||||
});
|
||||
};
|
||||
|
||||
const openWithdrawRecords = async () => {
|
||||
try {
|
||||
const list = await getUserWithdrawals(1001);
|
||||
|
||||
@@ -1038,10 +1038,19 @@ const submitReview = async () => {
|
||||
.order-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
.btn-report-link {
|
||||
font-size: 22rpx;
|
||||
color: #64748B;
|
||||
text-decoration: underline;
|
||||
margin-right: auto;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 60rpx;
|
||||
line-height: 58rpx;
|
||||
@@ -1395,6 +1404,72 @@ const submitReview = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.evidence-section {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.evidence-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-top: 12rpx;
|
||||
|
||||
.evidence-item {
|
||||
position: relative;
|
||||
width: 130rpx;
|
||||
height: 130rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.evidence-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.evidence-delete {
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
right: 6rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.close-icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.evidence-upload-btn {
|
||||
width: 130rpx;
|
||||
height: 130rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.upload-icon-svg {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.upload-txt {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
@@ -1416,6 +1491,11 @@ const submitReview = async () => {
|
||||
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
|
||||
|
||||
&.danger {
|
||||
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
|
||||
box-shadow: 0 4rpx 16rpx rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1482,6 +1562,19 @@ const submitReview = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.evidence-section {
|
||||
.evidence-upload-btn {
|
||||
background: #F5F2EC;
|
||||
border-color: #E7E5E4;
|
||||
.upload-txt {
|
||||
color: #78716C;
|
||||
}
|
||||
}
|
||||
.evidence-item {
|
||||
border-color: #E7E5E4;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
.modal-btn {
|
||||
&.cancel-btn {
|
||||
|
||||
@@ -936,20 +936,9 @@ const minWithdrawAmount = computed(() => {
|
||||
|
||||
const openWithdrawModal = () => {
|
||||
if (!ensureLoggedIn()) return;
|
||||
const comm = Number(user.value.commission || 0);
|
||||
if (comm <= 0) {
|
||||
uni.showToast({ title: '暂无可提现佣金', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
withdrawForm.value = {
|
||||
channel: 'WECHAT',
|
||||
amount: String(comm),
|
||||
alipayAccount: '',
|
||||
bankCard: '',
|
||||
bankName: '',
|
||||
realName: user.value.nickname || ''
|
||||
};
|
||||
showWithdrawModal.value = true;
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/withdraw'
|
||||
});
|
||||
};
|
||||
|
||||
const openWithdrawRecordsModal = async () => {
|
||||
|
||||
1373
pwuniapp/pages/user/withdraw.vue
普通文件
1373
pwuniapp/pages/user/withdraw.vue
普通文件
文件差异内容过多而无法显示
加载差异
127
server/tests/client-features.spec.ts
普通文件
127
server/tests/client-features.spec.ts
普通文件
@@ -0,0 +1,127 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ContentSecurityService } from '../src/core/security/content-security.service';
|
||||
|
||||
describe('ClientFeatures (TDD 测试套件: 用户端功能闭环)', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const uniPagesDir = path.join(rootDir, 'pwuniapp', 'pages');
|
||||
const serverSrcDir = path.join(rootDir, 'server', 'src');
|
||||
const contentSecurityService = new ContentSecurityService();
|
||||
|
||||
describe('1. 举报违规行为通道闭环验证', () => {
|
||||
it('后端 repository 必须实现 createReport 与 getReports 并支持存证入库', () => {
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||||
|
||||
expect(content).toContain('createReport');
|
||||
expect(content).toContain('getReports');
|
||||
expect(content).toContain('reports');
|
||||
expect(content).toContain('reporter_id');
|
||||
expect(content).toContain('target_type');
|
||||
});
|
||||
|
||||
it('后端 index.ts 必须暴露 POST /api/reports 路由并集成文本安全检测', () => {
|
||||
const indexPath = path.join(serverSrcDir, 'index.ts');
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
expect(content).toContain("app.post('/api/reports'");
|
||||
expect(content).toContain('contentSecurityService.checkText');
|
||||
expect(content).toContain('repo.createReport');
|
||||
});
|
||||
|
||||
it('违规线索说明命中涉黄/欺诈敏感词时,内容安全风控应拦截', () => {
|
||||
const checkResult = contentSecurityService.checkText('加微信私下转账免手续费直接打款');
|
||||
expect(checkResult.pass).toBe(false);
|
||||
expect(checkResult.code).toBe(87014);
|
||||
expect(checkResult.suggest).toBe('block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 订单待支付倒计时与退款仲裁工单闭环验证', () => {
|
||||
it('15分钟待支付倒计时计算逻辑应准确', () => {
|
||||
const createdAt = new Date().toISOString();
|
||||
const createdTime = new Date(createdAt).getTime();
|
||||
const expireTime = createdTime + 15 * 60 * 1000;
|
||||
const now = createdTime + 5 * 60 * 1000; // 过去了5分钟
|
||||
const remainingSeconds = Math.max(0, Math.floor((expireTime - now) / 1000));
|
||||
|
||||
expect(remainingSeconds).toBe(10 * 60); // 剩余 600 秒
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const seconds = remainingSeconds % 60;
|
||||
expect(`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`).toBe('10:00');
|
||||
});
|
||||
|
||||
it('后端 repository 必须实现 applyOrderRefund 并原子写入 arbitrations 仲裁表', () => {
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||||
|
||||
expect(content).toContain('applyOrderRefund');
|
||||
expect(content).toContain('争议仲裁中');
|
||||
expect(content).toContain('arbitrations');
|
||||
});
|
||||
|
||||
it('后端 index.ts POST /api/orders/:id/refund 必须接收理由与说明并执行风控拦截', () => {
|
||||
const indexPath = path.join(serverSrcDir, 'index.ts');
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
expect(content).toContain("app.post('/api/orders/:id/refund'");
|
||||
expect(content).toContain('reason');
|
||||
expect(content).toContain('contentSecurityService.checkText');
|
||||
expect(content).toContain('repo.applyOrderRefund');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. 个人资料性别与签名更新闭环验证', () => {
|
||||
it('后端 repository 必须在 updateUserProfile 中支持 gender 与 signature 字段处理', () => {
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||||
|
||||
expect(content).toContain('gender');
|
||||
expect(content).toContain('signature');
|
||||
expect(content).toContain('slice(0, 64)');
|
||||
});
|
||||
|
||||
it('后端 index.ts POST /api/users/:id/profile 必须支持性别白名单与个性签名风控', () => {
|
||||
const indexPath = path.join(serverSrcDir, 'index.ts');
|
||||
const content = fs.readFileSync(indexPath, 'utf-8');
|
||||
|
||||
expect(content).toContain("app.post('/api/users/:id/profile'");
|
||||
expect(content).toContain('signature');
|
||||
expect(content).toContain('contentSecurityService.checkText');
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 前端核心页面规范与结构完整性校验', () => {
|
||||
it('个人中心页面 (user/index.vue) 必须包含个性签名展示与性别修改入口', () => {
|
||||
const userVuePath = path.join(uniPagesDir, 'user', 'index.vue');
|
||||
const content = fs.readFileSync(userVuePath, 'utf-8');
|
||||
|
||||
expect(content).toContain('user.signature');
|
||||
expect(content).toContain('editForm.gender');
|
||||
expect(content).toContain('editForm.signature');
|
||||
expect(content).toContain('个性签名');
|
||||
});
|
||||
|
||||
it('订单中心页面 (order/list.vue) 必须包含倒计时徽章、退款仲裁工单与举报入口', () => {
|
||||
const orderVuePath = path.join(uniPagesDir, 'order', 'list.vue');
|
||||
const content = fs.readFileSync(orderVuePath, 'utf-8');
|
||||
|
||||
expect(content).toContain('countdown-badge');
|
||||
expect(content).toContain('getOrderCountdown');
|
||||
expect(content).toContain('openRefundModal');
|
||||
expect(content).toContain('openReportModal');
|
||||
expect(content).toContain('refundForm');
|
||||
expect(content).toContain('reportForm');
|
||||
});
|
||||
|
||||
it('陪玩详情页面 (expert/detail.vue) 必须包含违规举报操作入口与弹窗', () => {
|
||||
const detailVuePath = path.join(uniPagesDir, 'expert', 'detail.vue');
|
||||
const content = fs.readFileSync(detailVuePath, 'utf-8');
|
||||
|
||||
expect(content).toContain('openReportModal');
|
||||
expect(content).toContain('submitReportTicket');
|
||||
expect(content).toContain('presetReportReasons');
|
||||
expect(content).toContain('review-modal-mask');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
expect(Array.isArray(config.pages)).toBe(true);
|
||||
expect(config.pages.length).toBe(16);
|
||||
expect(config.pages.length).toBe(17);
|
||||
|
||||
for (const pageItem of config.pages) {
|
||||
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
|
||||
@@ -674,6 +674,53 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('15. 独立专属收益提现页面验证 (pages/user/withdraw.vue)', () => {
|
||||
it('pages.json 必须成功注册 pages/user/withdraw 且物理文件必须存在', () => {
|
||||
const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue');
|
||||
expect(fs.existsSync(wdPath)).toBe(true);
|
||||
const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json');
|
||||
const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
expect(pagesJson).toContain('pages/user/withdraw');
|
||||
expect(pagesJson).toContain('收益提现');
|
||||
});
|
||||
|
||||
it('独立提现页面必须支持微信零钱、支付宝、银行卡三渠道与金额输入校验', () => {
|
||||
const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue');
|
||||
const content = fs.readFileSync(wdPath, 'utf-8');
|
||||
expect(content).toContain('currentChannel');
|
||||
expect(content).toContain('WECHAT');
|
||||
expect(content).toContain('ALIPAY');
|
||||
expect(content).toContain('BANK');
|
||||
expect(content).toContain('withdrawAmount');
|
||||
expect(content).toContain('handleFillAllCommission');
|
||||
expect(content).toContain('withdrawCommission');
|
||||
expect(content).toContain('showRecordsDrawer');
|
||||
});
|
||||
|
||||
it('个人中心与工作台必须实现对专属提现页面的原生导航跳转', () => {
|
||||
const userPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
|
||||
const userContent = fs.readFileSync(userPath, 'utf-8');
|
||||
expect(userContent).toContain('/pages/user/withdraw');
|
||||
|
||||
const wsPath = path.join(rootDir, 'pwuniapp', 'pages', 'expert', 'workspace.vue');
|
||||
const wsContent = fs.readFileSync(wsPath, 'utf-8');
|
||||
expect(wsContent).toContain('/pages/user/withdraw');
|
||||
});
|
||||
|
||||
it('提现页面代码严禁包含任何系统 Emoji 与 showModal 4 字门禁违规', () => {
|
||||
const wdPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'withdraw.vue');
|
||||
const content = fs.readFileSync(wdPath, 'utf-8');
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
expect(emojiRegex.test(content)).toBe(false);
|
||||
|
||||
const modalMatches = content.matchAll(/(?:confirmText|cancelText):\s*['"]([^'"]+)['"]/g);
|
||||
for (const match of modalMatches) {
|
||||
const btnText = match[1];
|
||||
expect(btnText.length).toBeLessThanOrEqual(4);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户