diff --git a/admin/arco-design-pro-vite/src/locale/en-US.ts b/admin/arco-design-pro-vite/src/locale/en-US.ts index bbeff0f..e3fe327 100644 --- a/admin/arco-design-pro-vite/src/locale/en-US.ts +++ b/admin/arco-design-pro-vite/src/locale/en-US.ts @@ -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', diff --git a/admin/arco-design-pro-vite/src/locale/zh-CN.ts b/admin/arco-design-pro-vite/src/locale/zh-CN.ts index 23347a6..780ae1b 100644 --- a/admin/arco-design-pro-vite/src/locale/zh-CN.ts +++ b/admin/arco-design-pro-vite/src/locale/zh-CN.ts @@ -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': '金刚区导航品类', diff --git a/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts b/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts index 96c6004..de51c52 100644 --- a/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts +++ b/admin/arco-design-pro-vite/src/router/routes/modules/safety.ts @@ -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: ['*'], + }, + }, ], }; diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue new file mode 100644 index 0000000..8cc5d07 --- /dev/null +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/pwuniapp/common/api.js b/pwuniapp/common/api.js index 3fe9943..52239cc 100644 --- a/pwuniapp/common/api.js +++ b/pwuniapp/common/api.js @@ -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; + } +} + diff --git a/pwuniapp/pages/user/index.vue b/pwuniapp/pages/user/index.vue index 4268ab1..9a55447 100644 --- a/pwuniapp/pages/user/index.vue +++ b/pwuniapp/pages/user/index.vue @@ -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 }); } diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts index 65e4a1a..6d77d59 100644 --- a/server/src/db/mysql.ts +++ b/server/src/db/mysql.ts @@ -372,6 +372,21 @@ export async function initDatabase(): Promise { ) 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 { (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]); + } } diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index 002a6e9..a849ae0 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -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 { + 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 { + 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 { diff --git a/server/src/index.ts b/server/src/index.ts index 5e1a349..8d878f2 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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 { diff --git a/server/tests/platform-config.spec.ts b/server/tests/platform-config.spec.ts new file mode 100644 index 0000000..8cdb436 --- /dev/null +++ b/server/tests/platform-config.spec.ts @@ -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 = { + customer_service: { hotline: '400-888-6666', wechat: 'peiwan_vip_kefu' }, + safety_notice: { title: '官方公告', points: ['注意安全'] } + }; + + const remoteConfigs: Record = { + 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('官方公告'); + }); +});