refactor(activity): 移除活动品类与合规商圈的本地兜底数据,完全由后台配置驱动
- 发起组局页 games 初始改为空,仅从 nav_categories(status=1) 加载;加载失败/为空显示重试提示,禁止提交空品类 - 合规商圈初始改为空,仅从 safety_places(status=正常营业) 加载;无数据时提示手动填写地点 - 活动列表品类导航移除默认 3 个兜底品类 - 117 套件/764 用例全绿
这个提交包含在:
@@ -49,7 +49,7 @@
|
|||||||
<!-- 品类选择 -->
|
<!-- 品类选择 -->
|
||||||
<view class="form-card">
|
<view class="form-card">
|
||||||
<view class="card-label">选择活动品类</view>
|
<view class="card-label">选择活动品类</view>
|
||||||
<view class="game-pills">
|
<view class="game-pills" v-if="games.length > 0">
|
||||||
<view
|
<view
|
||||||
v-for="g in games"
|
v-for="g in games"
|
||||||
:key="g"
|
:key="g"
|
||||||
@@ -60,6 +60,9 @@
|
|||||||
{{ g }}
|
{{ g }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<text v-else class="config-empty-tip" @click="loadLivePlaces">
|
||||||
|
{{ configLoading ? '品类加载中…' : '暂无可选品类,点击重试' }}
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 基础信息录入 -->
|
<!-- 基础信息录入 -->
|
||||||
@@ -135,7 +138,7 @@
|
|||||||
<!-- 合规示范商圈一键点选 (防风控与合规保障) -->
|
<!-- 合规示范商圈一键点选 (防风控与合规保障) -->
|
||||||
<view class="quick-chip-row">
|
<view class="quick-chip-row">
|
||||||
<text class="chip-label">合规商圈:</text>
|
<text class="chip-label">合规商圈:</text>
|
||||||
<view class="quick-chips">
|
<view class="quick-chips" v-if="recommendPublicZones.length > 0">
|
||||||
<view
|
<view
|
||||||
v-for="zoneItem in recommendPublicZones.slice(0, 4)"
|
v-for="zoneItem in recommendPublicZones.slice(0, 4)"
|
||||||
:key="zoneItem"
|
:key="zoneItem"
|
||||||
@@ -146,6 +149,9 @@
|
|||||||
{{ zoneItem.split(' ')[0] }}
|
{{ zoneItem.split(' ')[0] }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<text v-else class="config-empty-tip" @click="loadLivePlaces">
|
||||||
|
{{ configLoading ? '商圈加载中…' : '暂无合规商圈,请手动填写地点(点击重试)' }}
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -367,8 +373,9 @@
|
|||||||
<text class="risk-desc">
|
<text class="risk-desc">
|
||||||
平台严格保障人身安全,严禁将私人住宅、酒店客房、封闭公寓等非公开场所作为履约地点。
|
平台严格保障人身安全,严禁将私人住宅、酒店客房、封闭公寓等非公开场所作为履约地点。
|
||||||
</text>
|
</text>
|
||||||
<text class="risk-sub-tip">请选择以下平台认证的示范公开商圈:</text>
|
<text class="risk-sub-tip" v-if="recommendPublicZones.length > 0">请选择以下平台认证的示范公开商圈:</text>
|
||||||
<view class="recommend-zones">
|
<text class="risk-sub-tip" v-else>暂无可推荐的合规商圈,请手动填写公开商业场所。</text>
|
||||||
|
<view class="recommend-zones" v-if="recommendPublicZones.length > 0">
|
||||||
<view
|
<view
|
||||||
v-for="zone in recommendPublicZones"
|
v-for="zone in recommendPublicZones"
|
||||||
:key="zone"
|
:key="zone"
|
||||||
@@ -396,7 +403,8 @@ import { createActivity, createOrder, payOrder as payOrderApi, requestWechatPaym
|
|||||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||||
import { ensureLoginOrRedirect } from '@/common/role.js';
|
import { ensureLoginOrRedirect } from '@/common/role.js';
|
||||||
|
|
||||||
const games = ref(['美食陪吃', '同城陪唱', '下午茶陪喝', '同城微醺', '同城桌游', '热门电竞']);
|
const games = ref([]);
|
||||||
|
const configLoading = ref(true);
|
||||||
const currentThemeKey = ref(getCurrentThemeKey());
|
const currentThemeKey = ref(getCurrentThemeKey());
|
||||||
|
|
||||||
const onThemeChanged = (key) => {
|
const onThemeChanged = (key) => {
|
||||||
@@ -426,7 +434,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
payType: 'PROJECT',
|
payType: 'PROJECT',
|
||||||
game: '美食陪吃',
|
game: '',
|
||||||
title: '',
|
title: '',
|
||||||
startTime: '',
|
startTime: '',
|
||||||
location: '',
|
location: '',
|
||||||
@@ -467,12 +475,7 @@ const dispatchHours = ref(2);
|
|||||||
const changeDispatchHours = (delta) => {
|
const changeDispatchHours = (delta) => {
|
||||||
dispatchHours.value = Math.min(8, Math.max(1, Number(dispatchHours.value) + delta));
|
dispatchHours.value = Math.min(8, Math.max(1, Number(dispatchHours.value) + delta));
|
||||||
};
|
};
|
||||||
const recommendPublicZones = ref([
|
const recommendPublicZones = ref([]);
|
||||||
'北京市朝阳区三里屯太古里 (AAA公开商圈)',
|
|
||||||
'北京市朝阳区工体纯K量贩KTV (正规量贩)',
|
|
||||||
'北京市朝阳区国贸CBD商业街 (阳光示范商圈)',
|
|
||||||
'北京市海淀区五道口购物中心 (高校商圈)'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const presetTimeOptions = ref([
|
const presetTimeOptions = ref([
|
||||||
'今晚 19:30',
|
'今晚 19:30',
|
||||||
@@ -565,30 +568,42 @@ const maxParticipants = ref(10);
|
|||||||
const presetPerPersonFees = ref([200, 300, 500, 800, 1000]);
|
const presetPerPersonFees = ref([200, 300, 500, 800, 1000]);
|
||||||
const presetTotalFees = ref([500, 1000, 2000, 3000, 5000]);
|
const presetTotalFees = ref([500, 1000, 2000, 3000, 5000]);
|
||||||
|
|
||||||
|
let placesRequestSeq = 0;
|
||||||
|
|
||||||
const loadLivePlaces = async () => {
|
const loadLivePlaces = async () => {
|
||||||
|
const seq = ++placesRequestSeq;
|
||||||
|
configLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const [placesList, catList, configsRes] = await Promise.allSettled([
|
const [placesList, catList, configsRes] = await Promise.allSettled([
|
||||||
getSafetyPlaces(),
|
getSafetyPlaces(),
|
||||||
getNavCategories(),
|
getNavCategories(),
|
||||||
getPlatformConfigs()
|
getPlatformConfigs()
|
||||||
]);
|
]);
|
||||||
|
if (seq !== placesRequestSeq) return;
|
||||||
|
|
||||||
if (placesList.status === 'fulfilled' && Array.isArray(placesList.value) && placesList.value.length > 0) {
|
// 商圈完全以后台「线下商圈场所库」为准,不再使用本地兜底数据
|
||||||
|
if (placesList.status === 'fulfilled' && Array.isArray(placesList.value)) {
|
||||||
recommendPublicZones.value = placesList.value
|
recommendPublicZones.value = placesList.value
|
||||||
.filter((p) => p.status === '正常营业')
|
.filter((p) => p.status === '正常营业')
|
||||||
.map((p) => `${p.area} · ${p.name}`);
|
.map((p) => `${p.area} · ${p.name}`);
|
||||||
|
} else {
|
||||||
|
recommendPublicZones.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (catList.status === 'fulfilled' && Array.isArray(catList.value) && catList.value.length > 0) {
|
// 活动品类完全以后台「金刚区导航品类」为准,不再使用本地兜底数据
|
||||||
|
if (catList.status === 'fulfilled' && Array.isArray(catList.value)) {
|
||||||
const activeCats = catList.value
|
const activeCats = catList.value
|
||||||
.filter((c) => c.status === 1 && c.name && c.name !== '全部活动')
|
.filter((c) => c.status === 1 && c.name && c.name !== '全部活动')
|
||||||
.map((c) => c.name);
|
.map((c) => c.name);
|
||||||
if (activeCats.length > 0) {
|
games.value = activeCats;
|
||||||
games.value = activeCats;
|
if (activeCats.length === 0) {
|
||||||
if (!games.value.includes(form.value.game)) {
|
form.value.game = '';
|
||||||
form.value.game = games.value[0];
|
} else if (!activeCats.includes(form.value.game)) {
|
||||||
}
|
form.value.game = activeCats[0];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
games.value = [];
|
||||||
|
form.value.game = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configsRes.status === 'fulfilled' && configsRes.value?.activity_rules) {
|
if (configsRes.status === 'fulfilled' && configsRes.value?.activity_rules) {
|
||||||
@@ -603,7 +618,16 @@ const loadLivePlaces = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[ActivityCreate] 加载合规场所与品类异常降级:', e);
|
console.warn('[ActivityCreate] 加载合规场所与品类异常:', e);
|
||||||
|
if (seq === placesRequestSeq) {
|
||||||
|
recommendPublicZones.value = [];
|
||||||
|
games.value = [];
|
||||||
|
form.value.game = '';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (seq === placesRequestSeq) {
|
||||||
|
configLoading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -957,6 +981,13 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-empty-tip {
|
||||||
|
display: block;
|
||||||
|
padding: 12rpx 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94A3B8;
|
||||||
|
}
|
||||||
|
|
||||||
.input-row {
|
.input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1449,6 +1480,10 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.create-activity-container.theme-warm {
|
.create-activity-container.theme-warm {
|
||||||
|
.form-card .config-empty-tip {
|
||||||
|
color: #78716C;
|
||||||
|
}
|
||||||
|
|
||||||
background-color: #FDFBF7;
|
background-color: #FDFBF7;
|
||||||
|
|
||||||
.mode-switch-wrap .mode-item {
|
.mode-switch-wrap .mode-item {
|
||||||
|
|||||||
@@ -296,12 +296,8 @@ const navCapsules = computed(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认兜底品类
|
// 品类完全以后台配置为准,不再使用本地兜底数据
|
||||||
return [
|
return [];
|
||||||
{ code: 'MEAL', catKey: 'offline_meal', name: '美食陪吃', iconSvg: ICONS.utensils },
|
|
||||||
{ code: 'SING', catKey: 'offline_sing', name: '同城陪唱', iconSvg: ICONS.micVocal },
|
|
||||||
{ code: 'DRINK', catKey: 'offline_drink', name: '下午茶陪喝', iconSvg: ICONS.coffee }
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
|
|||||||
@@ -302,6 +302,21 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
|||||||
expect(content).toContain('uni.chooseLocation');
|
expect(content).toContain('uni.chooseLocation');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('活动品类与合规商圈不得保留本地兜底数据,必须完全由后台配置驱动', () => {
|
||||||
|
const actCreatePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'create.vue');
|
||||||
|
const content = fs.readFileSync(actCreatePath, 'utf-8');
|
||||||
|
expect(content).toContain('const games = ref([]);');
|
||||||
|
expect(content).toContain('const recommendPublicZones = ref([]);');
|
||||||
|
expect(content).toContain('game: \'\'');
|
||||||
|
expect(content).not.toContain('北京市朝阳区三里屯太古里');
|
||||||
|
expect(content).not.toContain('北京市海淀区五道口购物中心');
|
||||||
|
expect(content).not.toContain("['美食陪吃', '同城陪唱'");
|
||||||
|
|
||||||
|
const actListPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'list.vue');
|
||||||
|
const listContent = fs.readFileSync(actListPath, 'utf-8');
|
||||||
|
expect(listContent).not.toContain('默认兜底品类');
|
||||||
|
});
|
||||||
|
|
||||||
it('所有新增代码与测试套件严禁包含任何 Emoji 字符', () => {
|
it('所有新增代码与测试套件严禁包含任何 Emoji 字符', () => {
|
||||||
const pathsToCheck = [
|
const pathsToCheck = [
|
||||||
path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'),
|
path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'),
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户