feat(system): implement dynamic platform configs for customer service, safety notice, and service agreement
这个提交包含在:
@@ -48,6 +48,7 @@ export default {
|
||||
'menu.safety': 'Safety & Risk Control',
|
||||
'menu.safety.places': 'Approved Venues',
|
||||
'menu.safety.auditLog': 'Risk Intercept Log',
|
||||
'menu.safety.platformConfig': 'Platform & Support Config',
|
||||
'menu.banner': 'Banner & Operations',
|
||||
'menu.banner.manage': 'Banner Management',
|
||||
'menu.banner.navCategory': 'Category Navigation',
|
||||
|
||||
@@ -48,6 +48,7 @@ export default {
|
||||
'menu.safety': '履约安全与风控',
|
||||
'menu.safety.places': '线下商圈场所库',
|
||||
'menu.safety.auditLog': '场所与内容拦截日志',
|
||||
'menu.safety.platformConfig': '客服与公告协议配置',
|
||||
'menu.banner': '首页轮播与运营',
|
||||
'menu.banner.manage': '轮播图管理',
|
||||
'menu.banner.navCategory': '金刚区导航品类',
|
||||
|
||||
@@ -32,6 +32,16 @@ const SAFETY: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'platform-config',
|
||||
name: 'PlatformConfig',
|
||||
component: () => import('@/views/safety/platform-config/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.safety.platformConfig',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.safety', 'menu.safety.platformConfig']" />
|
||||
<a-card class="general-card" :title="'平台客服与官方指引/协议运营配置'">
|
||||
<template #extra>
|
||||
<a-button type="primary" :loading="loading" @click="fetchConfigs">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新配置
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-tabs default-active-key="customer_service" type="rounded">
|
||||
<!-- Tab 1: 官方客服配置 -->
|
||||
<a-tab-pane key="customer_service" title="官方客服与热线配置">
|
||||
<div class="tab-pane-content">
|
||||
<a-alert type="info" style="margin-bottom: 20px">
|
||||
在此配置小程序个人中心“官方在线客服”弹层的联系电话与专属微信号,支持即时保存生效。
|
||||
</a-alert>
|
||||
|
||||
<a-form :model="csForm" layout="vertical" style="max-width: 650px">
|
||||
<a-form-item label="配置名称">
|
||||
<a-input v-model="csForm.name" placeholder="如:官方在线客服与服务热线" />
|
||||
</a-form-item>
|
||||
<a-form-item label="7x24h 官方客服服务热线">
|
||||
<a-input v-model="csForm.valueJson.hotline" placeholder="例如:400-888-6666" />
|
||||
</a-form-item>
|
||||
<a-form-item label="热线展示说明">
|
||||
<a-input v-model="csForm.valueJson.hotlineDesc" placeholder="例如:7x24h 官方客服服务热线" />
|
||||
</a-form-item>
|
||||
<a-form-item label="官方 VIP 客服微信号">
|
||||
<a-input v-model="csForm.valueJson.wechat" placeholder="例如:peiwan_vip_kefu" />
|
||||
</a-form-item>
|
||||
<a-form-item label="微信号展示说明">
|
||||
<a-input v-model="csForm.valueJson.wechatDesc" placeholder="例如:官方 VIP 客服微信号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="客服服务时间">
|
||||
<a-input v-model="csForm.valueJson.workHours" placeholder="例如:周一至周日 00:00 - 24:00" />
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
v-model="csForm.status"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="saving" @click="saveConfig('customer_service', csForm)">
|
||||
保存客服配置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 2: 官方安全履约公告 -->
|
||||
<a-tab-pane key="safety_notice" title="安全履约公告指引">
|
||||
<div class="tab-pane-content">
|
||||
<a-alert type="warning" style="margin-bottom: 20px">
|
||||
在此配置小程序个人中心“平台消息与通知”弹层的履约安全指引与风险防范须知。
|
||||
</a-alert>
|
||||
|
||||
<a-form :model="noticeForm" layout="vertical" style="max-width: 750px">
|
||||
<a-form-item label="公告标题">
|
||||
<a-input v-model="noticeForm.valueJson.title" placeholder="如:平台官方公告与指引" />
|
||||
</a-form-item>
|
||||
<a-form-item label="安全须知要点 (每行一条)">
|
||||
<a-textarea
|
||||
v-model="noticePointsText"
|
||||
placeholder="每行一条须知要点"
|
||||
:auto-size="{ minRows: 5, maxRows: 10 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
v-model="noticeForm.status"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="saving" @click="saveNoticeConfig">
|
||||
保存公告指引
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 3: 用户服务协议与规范 -->
|
||||
<a-tab-pane key="platform_protocol" title="平台规范与服务协议">
|
||||
<div class="tab-pane-content">
|
||||
<a-alert type="success" style="margin-bottom: 20px">
|
||||
在此配置“平台规范与服务协议”弹层的协议条款与合规内容,严格遵循微信内容安全风控标准。
|
||||
</a-alert>
|
||||
|
||||
<a-form :model="protoForm" layout="vertical" style="max-width: 800px">
|
||||
<a-form-item label="协议标题">
|
||||
<a-input v-model="protoForm.valueJson.title" placeholder="如:用户服务协议与隐私规范" />
|
||||
</a-form-item>
|
||||
<a-form-item label="协议摘要概要">
|
||||
<a-input v-model="protoForm.valueJson.summary" placeholder="协议概要,一两句话总结" />
|
||||
</a-form-item>
|
||||
<a-form-item label="服务协议全文内容">
|
||||
<a-textarea
|
||||
v-model="protoForm.valueJson.content"
|
||||
placeholder="请输入完整协议条款"
|
||||
:auto-size="{ minRows: 8, maxRows: 15 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用状态">
|
||||
<a-switch
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
v-model="protoForm.status"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="saving" @click="saveConfig('platform_protocol', protoForm)">
|
||||
保存协议规范
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const csForm = reactive({
|
||||
name: '官方在线客服与服务热线',
|
||||
description: '小程序端个人中心客服联系方式',
|
||||
status: 1,
|
||||
valueJson: {
|
||||
hotline: '400-888-6666',
|
||||
hotlineDesc: '7x24h 官方客服服务热线',
|
||||
wechat: 'peiwan_vip_kefu',
|
||||
wechatDesc: '官方 VIP 客服微信号',
|
||||
workHours: '周一至周日 00:00 - 24:00'
|
||||
}
|
||||
});
|
||||
|
||||
const noticeForm = reactive({
|
||||
name: '安全履约须知与官方公告',
|
||||
description: '小程序端官方指引与安全红线弹窗',
|
||||
status: 1,
|
||||
valueJson: {
|
||||
title: '平台官方公告与指引',
|
||||
points: [
|
||||
'【安全履约须知】为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。',
|
||||
'【现场核销】现场碰面后请出示 4 位数字核销码进行签到确认。',
|
||||
'【平台兜底】平台提供 7x24 小时争议仲裁与资金托管保障服务,严禁脱离平台私下交易。'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const noticePointsText = ref('');
|
||||
|
||||
const protoForm = reactive({
|
||||
name: '平台服务协议与合规规范',
|
||||
description: '小程序端服务协议与隐私政策详情',
|
||||
status: 1,
|
||||
valueJson: {
|
||||
title: '用户服务协议与隐私规范',
|
||||
summary: '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。',
|
||||
content: ''
|
||||
}
|
||||
});
|
||||
|
||||
const fetchConfigs = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data: res } = await axios.get('/api/admin/system/configs');
|
||||
if (res && res.code === 20000 && Array.isArray(res.data)) {
|
||||
res.data.forEach((item: any) => {
|
||||
if (item.configKey === 'customer_service') {
|
||||
csForm.name = item.name;
|
||||
csForm.description = item.description;
|
||||
csForm.status = item.status;
|
||||
csForm.valueJson = { ...csForm.valueJson, ...(item.valueJson || {}) };
|
||||
} else if (item.configKey === 'safety_notice') {
|
||||
noticeForm.name = item.name;
|
||||
noticeForm.description = item.description;
|
||||
noticeForm.status = item.status;
|
||||
noticeForm.valueJson = { ...noticeForm.valueJson, ...(item.valueJson || {}) };
|
||||
if (Array.isArray(noticeForm.valueJson.points)) {
|
||||
noticePointsText.value = noticeForm.valueJson.points.join('\n');
|
||||
}
|
||||
} else if (item.configKey === 'platform_protocol') {
|
||||
protoForm.name = item.name;
|
||||
protoForm.description = item.description;
|
||||
protoForm.status = item.status;
|
||||
protoForm.valueJson = { ...protoForm.valueJson, ...(item.valueJson || {}) };
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error(err.response?.data?.message || '获取系统配置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async (key: string, form: any) => {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
status: form.status,
|
||||
valueJson: form.valueJson
|
||||
};
|
||||
const { data: res } = await axios.put(`/api/admin/system/configs/${key}`, payload);
|
||||
if (res && res.code === 20000) {
|
||||
Message.success('配置已保存并同步至小程序端');
|
||||
fetchConfigs();
|
||||
} else {
|
||||
Message.error(res?.message || '保存配置失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error(err.response?.data?.message || '保存配置异常,请检查内容是否含敏感词');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveNoticeConfig = () => {
|
||||
const points = noticePointsText.value
|
||||
.split('\n')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => Boolean(p));
|
||||
noticeForm.valueJson.points = points;
|
||||
saveConfig('safety_notice', noticeForm);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfigs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
.general-card {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tab-pane-content {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -737,3 +737,16 @@ export async function checkTextSecurity(content) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台通用配置(官方客服、履约公告指引、平台规范协议等)
|
||||
*/
|
||||
export async function getPlatformConfigs() {
|
||||
try {
|
||||
const res = await http.get('/system/configs');
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取平台通用配置降级:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, setTheme, THEME_KEYS, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getCurrentRole, setCurrentRole, ROLE_TYPES, toggleRole as toggleUserRole, isExpertCertified, getExpertCertStatus, setExpertCertStatus, CERT_STATUS } from '@/common/role.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers } from '@/common/api.js';
|
||||
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs } from '@/common/api.js';
|
||||
|
||||
const user = ref({
|
||||
id: 1001,
|
||||
@@ -236,6 +236,40 @@ const currentRole = ref(getCurrentRole());
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const navInfo = ref(getNavBarInfo());
|
||||
const privacyModalRef = ref(null);
|
||||
const platformConfigs = ref({
|
||||
customer_service: {
|
||||
hotline: '400-888-6666',
|
||||
hotlineDesc: '7x24h 官方客服服务热线',
|
||||
wechat: 'peiwan_vip_kefu',
|
||||
wechatDesc: '官方 VIP 客服微信号'
|
||||
},
|
||||
safety_notice: {
|
||||
title: '平台官方公告与指引',
|
||||
points: [
|
||||
'【安全履约须知】为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。',
|
||||
'【现场核销】现场碰面后请出示 4 位数字核销码进行签到确认。',
|
||||
'【平台兜底】平台提供 7x24 小时争议仲裁与资金托管保障服务。'
|
||||
]
|
||||
},
|
||||
platform_protocol: {
|
||||
title: '用户服务协议与隐私规范',
|
||||
summary: '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。'
|
||||
}
|
||||
});
|
||||
|
||||
const loadPlatformConfigs = async () => {
|
||||
try {
|
||||
const res = await getPlatformConfigs();
|
||||
if (res && typeof res === 'object') {
|
||||
platformConfigs.value = {
|
||||
...platformConfigs.value,
|
||||
...res
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[User] 加载系统配置失败降级:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const navStyle = computed(() => {
|
||||
return {
|
||||
@@ -274,6 +308,7 @@ onMounted(() => {
|
||||
uni.$on('theme-change', onThemeChanged);
|
||||
uni.$on('role-change', onRoleChanged);
|
||||
loadUserProfile();
|
||||
loadPlatformConfigs();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -283,6 +318,7 @@ onShow(() => {
|
||||
applyThemeToNativeUI(currentThemeKey.value);
|
||||
uni.hideTabBar({ animation: false, fail: () => {} });
|
||||
loadUserProfile();
|
||||
loadPlatformConfigs();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -588,23 +624,28 @@ const applyExpert = () => {
|
||||
};
|
||||
|
||||
const contactService = () => {
|
||||
const cs = platformConfigs.value.customer_service || {};
|
||||
const hotline = cs.hotline || '400-888-6666';
|
||||
const wechat = cs.wechat || 'peiwan_vip_kefu';
|
||||
const cleanPhone = hotline.replace(/[^\d]/g, '');
|
||||
|
||||
uni.showActionSheet({
|
||||
itemList: [
|
||||
'拨打 7x24h 官方服务热线 (400-888-6666)',
|
||||
'复制平台 VIP 客服微信号 (peiwan_vip_kefu)',
|
||||
`拨打官方服务热线 (${hotline})`,
|
||||
`复制平台 VIP 客服微信号 (${wechat})`,
|
||||
'查看平台官方安全指引'
|
||||
],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '4008886666',
|
||||
phoneNumber: cleanPhone,
|
||||
fail: () => {
|
||||
uni.showToast({ title: '服务热线: 400-888-6666', icon: 'none' });
|
||||
uni.showToast({ title: `服务热线: ${hotline}`, icon: 'none' });
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.setClipboardData({
|
||||
data: 'peiwan_vip_kefu',
|
||||
data: wechat,
|
||||
success: () => {
|
||||
uni.showToast({ title: '官方客服微信号已复制', icon: 'success' });
|
||||
}
|
||||
@@ -617,9 +658,16 @@ const contactService = () => {
|
||||
};
|
||||
|
||||
const openNotice = () => {
|
||||
const notice = platformConfigs.value.safety_notice || {};
|
||||
const title = notice.title || '平台官方公告与指引';
|
||||
let content = '【安全履约须知】\n1. 为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。\n2. 现场碰面后请出示 4 位数字核销码进行签到确认。\n3. 平台提供 7x24 小时争议仲裁与资金托管保障服务。';
|
||||
if (Array.isArray(notice.points) && notice.points.length > 0) {
|
||||
content = notice.points.map((p, idx) => `${idx + 1}. ${p}`).join('\n');
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '平台官方公告与指引',
|
||||
content: '【安全履约须知】\n1. 为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。\n2. 现场碰面后请出示 4 位数字核销码进行签到确认。\n3. 平台提供 7x24 小时争议仲裁与资金托管保障服务。',
|
||||
title,
|
||||
content,
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
confirmColor: '#6366F1'
|
||||
@@ -627,12 +675,13 @@ const openNotice = () => {
|
||||
};
|
||||
|
||||
const openAbout = () => {
|
||||
const proto = platformConfigs.value.platform_protocol || {};
|
||||
if (privacyModalRef.value && privacyModalRef.value.show) {
|
||||
privacyModalRef.value.show();
|
||||
} else {
|
||||
uni.showModal({
|
||||
title: '用户服务协议与隐私规范',
|
||||
content: '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。',
|
||||
title: proto.title || '用户服务协议与隐私规范',
|
||||
content: proto.content || proto.summary || '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。',
|
||||
showCancel: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -372,6 +372,21 @@ export async function initDatabase(): Promise<void> {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 20. 平台官方客服、公告及服务协议通用配置表
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS platform_configs (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
config_key VARCHAR(64) UNIQUE NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
value_json JSON NOT NULL,
|
||||
description VARCHAR(255) DEFAULT '',
|
||||
status TINYINT DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_key (config_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 初始化配置单条记录
|
||||
const [cfgRows]: any = await conn.query('SELECT COUNT(*) as count FROM finance_config WHERE id = 1');
|
||||
if (cfgRows[0].count === 0) {
|
||||
@@ -626,4 +641,40 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
(4, '电竞开黑连胜冲星券', 10.00, 30.00, 30, 5000, 340, 'ALL', '线上游戏陪玩下单立减', 1);
|
||||
`);
|
||||
}
|
||||
|
||||
// 17. 种子平台通用配置(官方客服、履约公告指引、规范协议)
|
||||
const [cfgItemRows]: any = await conn.query('SELECT COUNT(*) as count FROM platform_configs');
|
||||
if (cfgItemRows[0].count === 0) {
|
||||
const customerServiceVal = JSON.stringify({
|
||||
hotline: '400-888-6666',
|
||||
hotlineDesc: '7x24h 官方客服服务热线',
|
||||
wechat: 'peiwan_vip_kefu',
|
||||
wechatDesc: '官方 VIP 客服微信号',
|
||||
workHours: '周一至周日 00:00 - 24:00'
|
||||
});
|
||||
|
||||
const safetyNoticeVal = JSON.stringify({
|
||||
title: '平台官方公告与指引',
|
||||
points: [
|
||||
'【安全履约须知】为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。',
|
||||
'【现场核销】现场碰面后请出示 4 位数字核销码进行签到确认。',
|
||||
'【平台兜底】平台提供 7x24 小时争议仲裁与资金托管保障服务,严禁脱离平台私下交易。'
|
||||
],
|
||||
noticePopup: false
|
||||
});
|
||||
|
||||
const protocolVal = JSON.stringify({
|
||||
title: '用户服务协议与隐私规范',
|
||||
summary: '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。',
|
||||
content: '1. 账号使用规范:用户注册与使用陪玩服务须符合国家法律法规,严禁利用平台从事涉黄、赌博、欺诈等违规行为。\n2. 资金与交易保障:平台提供全额担保交易,订单完成前款项由官方账户托管,核销确认后自动结算至大神钱包。\n3. 争议与售后:若遇服务不符或搭子违约,可随时提交争议仲裁由运营平台人工介入裁决。'
|
||||
});
|
||||
|
||||
await conn.query(`
|
||||
INSERT INTO platform_configs (config_key, name, value_json, description, status)
|
||||
VALUES
|
||||
('customer_service', '官方在线客服与服务热线', ?, '小程序端个人中心客服联系方式', 1),
|
||||
('safety_notice', '安全履约须知与官方公告', ?, '小程序端官方指引与安全红线弹窗', 1),
|
||||
('platform_protocol', '平台服务协议与合规规范', ?, '小程序端服务协议与隐私政策详情', 1);
|
||||
`, [customerServiceVal, safetyNoticeVal, protocolVal]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,6 +1247,91 @@ export class MysqlRepository {
|
||||
return res.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 3. 平台通用配置与指引 (Platform Configs)
|
||||
async getPlatformConfigs(): Promise<{ [key: string]: any }> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT config_key as configKey, name, value_json as valueJson, description, status
|
||||
FROM platform_configs
|
||||
WHERE status = 1
|
||||
`);
|
||||
const result: { [key: string]: any } = {};
|
||||
for (const r of rows) {
|
||||
let val = r.valueJson;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
val = JSON.parse(val);
|
||||
} catch (_) {}
|
||||
}
|
||||
result[r.configKey] = val;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAdminPlatformConfigs(): Promise<any[]> {
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT id, config_key as configKey, name, value_json as valueJson, description, status,
|
||||
created_at as createdAt, updated_at as updatedAt
|
||||
FROM platform_configs
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
return rows.map((r: any) => {
|
||||
let val = r.valueJson;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
val = JSON.parse(val);
|
||||
} catch (_) {}
|
||||
}
|
||||
return {
|
||||
...r,
|
||||
valueJson: val
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async updatePlatformConfig(configKey: string, data: { name?: string; valueJson?: any; description?: string; status?: number }): Promise<any> {
|
||||
const fields: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (data.name !== undefined) {
|
||||
fields.push('name = ?');
|
||||
values.push(data.name);
|
||||
}
|
||||
if (data.valueJson !== undefined) {
|
||||
fields.push('value_json = ?');
|
||||
values.push(JSON.stringify(data.valueJson));
|
||||
}
|
||||
if (data.description !== undefined) {
|
||||
fields.push('description = ?');
|
||||
values.push(data.description);
|
||||
}
|
||||
if (data.status !== undefined) {
|
||||
fields.push('status = ?');
|
||||
values.push(Number(data.status));
|
||||
}
|
||||
|
||||
if (fields.length > 0) {
|
||||
values.push(configKey);
|
||||
await pool.query(`UPDATE platform_configs SET ${fields.join(', ')} WHERE config_key = ?`, values);
|
||||
}
|
||||
|
||||
const [rows]: any = await pool.query(`
|
||||
SELECT id, config_key as configKey, name, value_json as valueJson, description, status,
|
||||
created_at as createdAt, updated_at as updatedAt
|
||||
FROM platform_configs
|
||||
WHERE config_key = ?
|
||||
`, [configKey]);
|
||||
|
||||
if (!rows || rows.length === 0) return null;
|
||||
const r = rows[0];
|
||||
let val = r.valueJson;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
val = JSON.parse(val);
|
||||
} catch (_) {}
|
||||
}
|
||||
return { ...r, valueJson: val };
|
||||
}
|
||||
|
||||
|
||||
|
||||
async getArbitrations(): Promise<any[]> {
|
||||
|
||||
@@ -1051,6 +1051,57 @@ app.delete('/api/admin/coupons/:id', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 平台官方客服、履约公告与规范协议配置接口
|
||||
// 移动端:获取全部有效系统通用配置
|
||||
app.get('/api/system/configs', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const configs = await repo.getPlatformConfigs();
|
||||
res.json({ code: 20000, data: configs });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 管理后台:获取全部配置项列表
|
||||
app.get('/api/admin/system/configs', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const list = await repo.getAdminPlatformConfigs();
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 管理后台:更新指定配置项
|
||||
app.put('/api/admin/system/configs/:key', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const configKey = String(req.params.key);
|
||||
const { name, valueJson, description, status } = req.body;
|
||||
|
||||
// 微信内容安全审查(检查文本内容或字符串化后的配置)
|
||||
const textToCheck = `${name || ''} ${description || ''} ${JSON.stringify(valueJson || '')}`;
|
||||
const sec = contentSecurityService.checkText(textToCheck);
|
||||
if (!sec.pass) {
|
||||
return res.status(400).json({ code: sec.code, message: sec.message });
|
||||
}
|
||||
|
||||
const updated = await repo.updatePlatformConfig(configKey, {
|
||||
name,
|
||||
valueJson,
|
||||
description,
|
||||
status: status !== undefined ? Number(status) : undefined
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
return res.status(404).json({ code: 40400, message: '配置项不存在' });
|
||||
}
|
||||
|
||||
res.json({ code: 20000, data: updated, message: '系统配置更新成功' });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ code: 40000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新订单 (落盘 MySQL)
|
||||
app.post('/api/orders', async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// 已读取AGENTS.md
|
||||
import { ContentSecurityService } from '../src/core/security/content-security.service';
|
||||
|
||||
describe('平台通用配置与合规管理 TDD 自动化测试', () => {
|
||||
const secService = new ContentSecurityService();
|
||||
|
||||
it('1. 平台默认客服与公告配置数据结构验证', () => {
|
||||
const customerService = {
|
||||
hotline: '400-888-6666',
|
||||
hotlineDesc: '7x24h 官方客服服务热线',
|
||||
wechat: 'peiwan_vip_kefu',
|
||||
wechatDesc: '官方 VIP 客服微信号',
|
||||
workHours: '周一至周日 00:00 - 24:00'
|
||||
};
|
||||
|
||||
expect(customerService.hotline).toBe('400-888-6666');
|
||||
expect(customerService.wechat).toBe('peiwan_vip_kefu');
|
||||
|
||||
const safetyNotice = {
|
||||
title: '平台官方公告与指引',
|
||||
points: [
|
||||
'【安全履约须知】为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。',
|
||||
'【现场核销】现场碰面后请出示 4 位数字核销码进行签到确认。',
|
||||
'【平台兜底】平台提供 7x24 小时争议仲裁与资金托管保障服务,严禁脱离平台私下交易。'
|
||||
]
|
||||
};
|
||||
|
||||
expect(safetyNotice.points.length).toBe(3);
|
||||
expect(safetyNotice.points[0]).toContain('公开商业街区');
|
||||
});
|
||||
|
||||
it('2. 更新公告与客服信息时实施微信内容安全审查 (合规文本应放行)', () => {
|
||||
const safeData = {
|
||||
hotline: '400-999-8888',
|
||||
wechat: 'peiwan_official_service',
|
||||
points: ['请大家在公共场合文明约单,遵守平台履约秩序']
|
||||
};
|
||||
const check = secService.checkText(JSON.stringify(safeData));
|
||||
expect(check.pass).toBe(true);
|
||||
expect(check.suggest).toBe('pass');
|
||||
});
|
||||
|
||||
it('3. 更新配置时拦截涉黄违规词 (security.msgSecCheck 拦截)', () => {
|
||||
const vulgarConfig = {
|
||||
wechat: 'fake_kefu',
|
||||
content: '添加客服微信提供上门特殊服务'
|
||||
};
|
||||
const check = secService.checkText(JSON.stringify(vulgarConfig));
|
||||
expect(check.pass).toBe(false);
|
||||
expect(check.code).toBe(87014);
|
||||
expect(check.label).toBe('VULGAR_PORN');
|
||||
expect(check.message).toContain('上门特殊服务');
|
||||
});
|
||||
|
||||
it('4. 更新配置时拦截欺诈导流或线下私单 (防脱离平台交易)', () => {
|
||||
const fraudConfig = {
|
||||
notice: '联系微信号,私下转账免手续费支持线下私单优惠'
|
||||
};
|
||||
const check = secService.checkText(JSON.stringify(fraudConfig));
|
||||
expect(check.pass).toBe(false);
|
||||
expect(check.code).toBe(87014);
|
||||
expect(check.label).toBe('FRAUD_SCAM');
|
||||
});
|
||||
|
||||
it('5. 系统配置合并与动态降级容错机制', () => {
|
||||
const defaultConfigs: Record<string, any> = {
|
||||
customer_service: { hotline: '400-888-6666', wechat: 'peiwan_vip_kefu' },
|
||||
safety_notice: { title: '官方公告', points: ['注意安全'] }
|
||||
};
|
||||
|
||||
const remoteConfigs: Record<string, any> = {
|
||||
customer_service: { hotline: '400-123-4567', wechat: 'peiwan_kefu_02' }
|
||||
};
|
||||
|
||||
const merged = {
|
||||
...defaultConfigs,
|
||||
...remoteConfigs
|
||||
};
|
||||
|
||||
expect(merged.customer_service.hotline).toBe('400-123-4567');
|
||||
expect(merged.safety_notice.title).toBe('官方公告');
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户