From 207b52275497f779f77958e9a2b8f3a4b7ce14bb Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 5 Feb 2026 05:00:25 -0800 Subject: [PATCH] =?UTF-8?q?feat(customer-service):=20=E5=AE=A2=E6=9C=8D?= =?UTF-8?q?=E8=81=94=E7=B3=BB=E6=96=B9=E5=BC=8F=E4=BB=8E=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E6=94=B9=E4=B8=BA=E5=90=8E=E5=8F=B0=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 mobile-app "联系客服" 弹窗的微信/QQ联系方式从硬编码改为 admin-web 后台动态配置,支持任意数量的联系方式管理。 ## Backend (admin-service) - 新增 Prisma 模型: ContactType 枚举(WECHAT/QQ) + CustomerServiceContact - 新增迁移 SQL: 建表 + 2条索引 + 4条种子数据(保留现有硬编码联系方式) - 新增双 Controller (参考 app-asset.controller.ts 模式): - AdminCustomerServiceContactController (admin/customer-service-contacts) GET 列表 / POST 新增 / PUT 更新 / DELETE 删除 - PublicCustomerServiceContactController (customer-service-contacts) GET 仅返回 isEnabled=true,按 sortOrder 排序 - 注意: 公开 Controller 用 @Controller('customer-service-contacts') 避免与全局前缀 api/v1 双重叠加 ## Kong 网关 - 新增路由 admin-customer-service-contacts-public 路径 /api/v1/customer-service-contacts → admin-service:3010 - Admin 端点由已有 admin-api 路由 (/api/v1/admin) 覆盖 ## Admin-web - endpoints.ts: 新增 CUSTOMER_SERVICE_CONTACTS 端点组 - customerServiceContactService.ts: CRUD 服务 (list/create/update/delete) - settings/page.tsx: 新增"客服联系方式管理"区块 表格展示(排序/类型/标签/联系方式/启停/操作) + 内联新增/编辑表单 - settings.module.scss: contactTable / contactForm / contactFormFields 样式 ## Flutter Mobile-app - storage_keys.dart: 新增 cachedCustomerServiceContacts 缓存 key - customer_service_contact_service.dart: API + 缓存服务 (内存5分钟TTL + SharedPreferences持久化 + 后台静默刷新) - injection_container.dart: 注册 customerServiceContactServiceProvider - profile_page.dart: _showCustomerServiceDialog() 从硬编码改为 动态 API 加载,contacts 为空时显示"暂无客服联系方式"占位符 ## 文件清单 (4 新建 + 9 修改) 新建: - backend/.../migrations/20260205100000_add_customer_service_contacts/migration.sql - backend/.../controllers/customer-service-contact.controller.ts - frontend/admin-web/src/services/customerServiceContactService.ts - frontend/mobile-app/lib/core/services/customer_service_contact_service.dart 修改: - backend/.../prisma/schema.prisma - backend/.../src/app.module.ts - backend/api-gateway/kong.yml - frontend/admin-web/src/infrastructure/api/endpoints.ts - frontend/admin-web/src/app/(dashboard)/settings/page.tsx - frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss - frontend/mobile-app/lib/core/storage/storage_keys.dart - frontend/mobile-app/lib/core/di/injection_container.dart - frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart Co-Authored-By: Claude Opus 4.5 --- backend/api-gateway/kong.yml | 4 + .../migration.sql | 43 ++++ .../admin-service/prisma/schema.prisma | 26 +++ .../customer-service-contact.controller.ts | 196 ++++++++++++++++ .../services/admin-service/src/app.module.ts | 7 +- .../src/app/(dashboard)/settings/page.tsx | 210 +++++++++++++++++- .../(dashboard)/settings/settings.module.scss | 48 ++++ .../src/infrastructure/api/endpoints.ts | 8 + .../services/customerServiceContactService.ts | 55 +++++ .../lib/core/di/injection_container.dart | 8 + .../customer_service_contact_service.dart | 144 ++++++++++++ .../lib/core/storage/storage_keys.dart | 1 + .../presentation/pages/profile_page.dart | 79 ++++--- 13 files changed, 790 insertions(+), 39 deletions(-) create mode 100644 backend/services/admin-service/prisma/migrations/20260205100000_add_customer_service_contacts/migration.sql create mode 100644 backend/services/admin-service/src/api/controllers/customer-service-contact.controller.ts create mode 100644 frontend/admin-web/src/services/customerServiceContactService.ts create mode 100644 frontend/mobile-app/lib/core/services/customer_service_contact_service.dart diff --git a/backend/api-gateway/kong.yml b/backend/api-gateway/kong.yml index 35ecee9a..39c6423c 100644 --- a/backend/api-gateway/kong.yml +++ b/backend/api-gateway/kong.yml @@ -236,6 +236,10 @@ services: paths: - /api/v1/system-config strip_path: false + - name: admin-customer-service-contacts-public + paths: + - /api/v1/customer-service-contacts + strip_path: false # --------------------------------------------------------------------------- # Presence Service - 在线状态服务 diff --git a/backend/services/admin-service/prisma/migrations/20260205100000_add_customer_service_contacts/migration.sql b/backend/services/admin-service/prisma/migrations/20260205100000_add_customer_service_contacts/migration.sql new file mode 100644 index 00000000..2107aff1 --- /dev/null +++ b/backend/services/admin-service/prisma/migrations/20260205100000_add_customer_service_contacts/migration.sql @@ -0,0 +1,43 @@ +-- ============================================================================= +-- Customer Service Contacts Migration +-- 客服联系方式管理 +-- ============================================================================= + +-- ----------------------------------------------------------------------------- +-- 1. 客服联系方式类型枚举 +-- ----------------------------------------------------------------------------- + +CREATE TYPE "ContactType" AS ENUM ('WECHAT', 'QQ'); + +-- ----------------------------------------------------------------------------- +-- 2. 客服联系方式表 +-- ----------------------------------------------------------------------------- + +CREATE TABLE "customer_service_contacts" ( + "id" TEXT NOT NULL, + "type" "ContactType" NOT NULL, + "label" VARCHAR(100) NOT NULL, + "value" VARCHAR(200) NOT NULL, + "sort_order" INTEGER NOT NULL, + "is_enabled" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "customer_service_contacts_pkey" PRIMARY KEY ("id") +); + +-- 按类型和启用状态查询索引 +CREATE INDEX "customer_service_contacts_type_is_enabled_idx" ON "customer_service_contacts"("type", "is_enabled"); + +-- 按排序查询索引 +CREATE INDEX "customer_service_contacts_sort_order_idx" ON "customer_service_contacts"("sort_order"); + +-- ----------------------------------------------------------------------------- +-- 3. 初始数据 (保留现有硬编码的联系方式) +-- ----------------------------------------------------------------------------- + +INSERT INTO "customer_service_contacts" ("id", "type", "label", "value", "sort_order", "is_enabled", "created_at", "updated_at") VALUES + (gen_random_uuid(), 'WECHAT', '客服微信1', 'liulianhuanghou1', 1, true, NOW(), NOW()), + (gen_random_uuid(), 'WECHAT', '客服微信2', 'liulianhuanghou2', 2, true, NOW(), NOW()), + (gen_random_uuid(), 'QQ', '客服QQ1', '1502109619', 3, true, NOW(), NOW()), + (gen_random_uuid(), 'QQ', '客服QQ2', '2171447109', 4, true, NOW(), NOW()); diff --git a/backend/services/admin-service/prisma/schema.prisma b/backend/services/admin-service/prisma/schema.prisma index 245f4f7a..1312be6a 100644 --- a/backend/services/admin-service/prisma/schema.prisma +++ b/backend/services/admin-service/prisma/schema.prisma @@ -1177,3 +1177,29 @@ model AppAsset { @@index([type, isEnabled]) @@map("app_assets") } + +// ============================================================================= +// Customer Service Contacts (客服联系方式) +// ============================================================================= + +/// 客服联系方式类型 +enum ContactType { + WECHAT // 微信 + QQ // QQ +} + +/// 客服联系方式 - 管理员配置的客服联系信息 +model CustomerServiceContact { + id String @id @default(uuid()) + type ContactType + label String @db.VarChar(100) + value String @db.VarChar(200) + sortOrder Int @map("sort_order") + isEnabled Boolean @default(true) @map("is_enabled") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([type, isEnabled]) + @@index([sortOrder]) + @@map("customer_service_contacts") +} diff --git a/backend/services/admin-service/src/api/controllers/customer-service-contact.controller.ts b/backend/services/admin-service/src/api/controllers/customer-service-contact.controller.ts new file mode 100644 index 00000000..cbb6d509 --- /dev/null +++ b/backend/services/admin-service/src/api/controllers/customer-service-contact.controller.ts @@ -0,0 +1,196 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + HttpCode, + HttpStatus, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common' +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger' +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service' +import { ContactType } from '@prisma/client' + +// ===== DTOs ===== + +interface CreateContactDto { + type: string + label: string + value: string + sortOrder: number + isEnabled?: boolean +} + +interface UpdateContactDto { + type?: string + label?: string + value?: string + sortOrder?: number + isEnabled?: boolean +} + +interface ContactResponseDto { + id: string + type: ContactType + label: string + value: string + sortOrder: number + isEnabled: boolean + createdAt: Date + updatedAt: Date +} + +// ============================================================================= +// Admin Controller (需要认证) +// ============================================================================= + +@ApiTags('Customer Service Contact Management') +@Controller('admin/customer-service-contacts') +export class AdminCustomerServiceContactController { + private readonly logger = new Logger(AdminCustomerServiceContactController.name) + + constructor(private readonly prisma: PrismaService) {} + + @Get() + @ApiBearerAuth() + @ApiOperation({ summary: '查询客服联系方式列表 (全部)' }) + async list(): Promise { + const contacts = await this.prisma.customerServiceContact.findMany({ + orderBy: [{ sortOrder: 'asc' }], + }) + return contacts.map(this.toDto) + } + + @Post() + @ApiBearerAuth() + @ApiOperation({ summary: '新增客服联系方式' }) + async create(@Body() body: CreateContactDto): Promise { + const contactType = body.type as ContactType + if (!Object.values(ContactType).includes(contactType)) { + throw new BadRequestException(`Invalid type: ${body.type}. Must be WECHAT or QQ`) + } + if (!body.label || !body.value) { + throw new BadRequestException('label and value are required') + } + if (body.sortOrder === undefined || body.sortOrder < 0) { + throw new BadRequestException('sortOrder must be a non-negative integer') + } + + const contact = await this.prisma.customerServiceContact.create({ + data: { + type: contactType, + label: body.label, + value: body.value, + sortOrder: body.sortOrder, + isEnabled: body.isEnabled ?? true, + }, + }) + + this.logger.log(`Created customer service contact: id=${contact.id}, type=${contact.type}, label=${contact.label}`) + return this.toDto(contact) + } + + @Put(':id') + @ApiBearerAuth() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '更新客服联系方式' }) + async update( + @Param('id') id: string, + @Body() body: UpdateContactDto, + ): Promise { + const existing = await this.prisma.customerServiceContact.findUnique({ where: { id } }) + if (!existing) { + throw new NotFoundException('Contact not found') + } + + const data: Record = {} + if (body.type !== undefined) { + const contactType = body.type as ContactType + if (!Object.values(ContactType).includes(contactType)) { + throw new BadRequestException(`Invalid type: ${body.type}`) + } + data.type = contactType + } + if (body.label !== undefined) data.label = body.label + if (body.value !== undefined) data.value = body.value + if (body.sortOrder !== undefined) data.sortOrder = body.sortOrder + if (body.isEnabled !== undefined) data.isEnabled = body.isEnabled + + const updated = await this.prisma.customerServiceContact.update({ + where: { id }, + data, + }) + + return this.toDto(updated) + } + + @Delete(':id') + @ApiBearerAuth() + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: '删除客服联系方式' }) + async delete(@Param('id') id: string): Promise { + const existing = await this.prisma.customerServiceContact.findUnique({ where: { id } }) + if (!existing) { + throw new NotFoundException('Contact not found') + } + await this.prisma.customerServiceContact.delete({ where: { id } }) + this.logger.log(`Deleted customer service contact: id=${id}, type=${existing.type}`) + } + + private toDto(contact: { + id: string + type: ContactType + label: string + value: string + sortOrder: number + isEnabled: boolean + createdAt: Date + updatedAt: Date + }): ContactResponseDto { + return { + id: contact.id, + type: contact.type, + label: contact.label, + value: contact.value, + sortOrder: contact.sortOrder, + isEnabled: contact.isEnabled, + createdAt: contact.createdAt, + updatedAt: contact.updatedAt, + } + } +} + +// ============================================================================= +// Public Controller (移动端调用,无需认证) +// ============================================================================= + +@ApiTags('Customer Service Contacts (Public)') +@Controller('customer-service-contacts') +export class PublicCustomerServiceContactController { + constructor(private readonly prisma: PrismaService) {} + + @Get() + @ApiOperation({ summary: '获取已启用的客服联系方式 (移动端)' }) + async list(): Promise { + const contacts = await this.prisma.customerServiceContact.findMany({ + where: { isEnabled: true }, + orderBy: [{ sortOrder: 'asc' }], + }) + + return contacts.map((c) => ({ + id: c.id, + type: c.type, + label: c.label, + value: c.value, + sortOrder: c.sortOrder, + isEnabled: c.isEnabled, + createdAt: c.createdAt, + updatedAt: c.updatedAt, + })) + } +} diff --git a/backend/services/admin-service/src/app.module.ts b/backend/services/admin-service/src/app.module.ts index e918c3c2..a9c78794 100644 --- a/backend/services/admin-service/src/app.module.ts +++ b/backend/services/admin-service/src/app.module.ts @@ -77,7 +77,9 @@ import { SystemMaintenanceRepositoryImpl } from './infrastructure/persistence/re import { AdminMaintenanceController, MobileMaintenanceController } from './api/controllers/system-maintenance.controller'; import { MaintenanceInterceptor } from './api/interceptors/maintenance.interceptor'; // App Asset imports -import { AdminAppAssetController, PublicAppAssetController } from './api/controllers/app-asset.controller'; +import { AdminAppAssetController, PublicAppAssetController } from './api/controllers/app-asset.controller' +// Customer Service Contact imports +import { AdminCustomerServiceContactController, PublicCustomerServiceContactController } from './api/controllers/customer-service-contact.controller'; @Module({ imports: [ @@ -116,6 +118,9 @@ import { AdminAppAssetController, PublicAppAssetController } from './api/control // App Asset Controllers AdminAppAssetController, PublicAppAssetController, + // Customer Service Contact Controllers + AdminCustomerServiceContactController, + PublicCustomerServiceContactController, ], providers: [ PrismaService, diff --git a/frontend/admin-web/src/app/(dashboard)/settings/page.tsx b/frontend/admin-web/src/app/(dashboard)/settings/page.tsx index e6f736aa..1baee5a1 100644 --- a/frontend/admin-web/src/app/(dashboard)/settings/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/settings/page.tsx @@ -6,6 +6,7 @@ import { cn } from '@/utils/helpers'; import styles from './settings.module.scss'; import { systemConfigService, type DisplaySettings } from '@/services/systemConfigService'; import { appAssetService, type AppAsset, type AppAssetType } from '@/services/appAssetService'; +import { customerServiceContactService, type CustomerServiceContact, type ContactType } from '@/services/customerServiceContactService'; /** * 后台账号数据接口 @@ -96,6 +97,12 @@ export default function SettingsPage() { const [assetsLoading, setAssetsLoading] = useState(false); const [uploadingSlot, setUploadingSlot] = useState(null); + // 客服联系方式管理 + const [contacts, setContacts] = useState([]); + const [contactsLoading, setContactsLoading] = useState(false); + const [editingContact, setEditingContact] = useState | null>(null); + const [contactSaving, setContactSaving] = useState(false); + // 后台账号与安全 const [approvalCount, setApprovalCount] = useState('3'); const [sensitiveOperations, setSensitiveOperations] = useState(['修改结算参数', '删除用户']); @@ -205,11 +212,82 @@ export default function SettingsPage() { } }, [loadAppAssets]); + // 加载客服联系方式 + const loadContacts = useCallback(async () => { + setContactsLoading(true); + try { + const list = await customerServiceContactService.list(); + setContacts(Array.isArray(list) ? list.sort((a: CustomerServiceContact, b: CustomerServiceContact) => a.sortOrder - b.sortOrder) : []); + } catch (error) { + console.error('Failed to load contacts:', error); + } finally { + setContactsLoading(false); + } + }, []); + + // 保存客服联系方式 (新增或编辑) + const handleContactSave = useCallback(async () => { + if (!editingContact) return; + if (!editingContact.type || !editingContact.label || !editingContact.value) { + alert('请填写完整信息'); + return; + } + setContactSaving(true); + try { + if (editingContact.id) { + await customerServiceContactService.update(editingContact.id, { + type: editingContact.type as ContactType, + label: editingContact.label, + value: editingContact.value, + sortOrder: editingContact.sortOrder ?? 0, + isEnabled: editingContact.isEnabled, + }); + } else { + await customerServiceContactService.create({ + type: editingContact.type as ContactType, + label: editingContact.label, + value: editingContact.value, + sortOrder: editingContact.sortOrder ?? contacts.length, + isEnabled: editingContact.isEnabled ?? true, + }); + } + setEditingContact(null); + await loadContacts(); + } catch (error) { + console.error('Save contact failed:', error); + alert('保存失败,请重试'); + } finally { + setContactSaving(false); + } + }, [editingContact, contacts.length, loadContacts]); + + // 删除客服联系方式 + const handleContactDelete = useCallback(async (id: string) => { + if (!confirm('确定要删除此联系方式吗?')) return; + try { + await customerServiceContactService.delete(id); + await loadContacts(); + } catch (error) { + console.error('Delete contact failed:', error); + } + }, [loadContacts]); + + // 切换客服联系方式启停 + const handleContactToggle = useCallback(async (id: string, isEnabled: boolean) => { + try { + await customerServiceContactService.update(id, { isEnabled }); + await loadContacts(); + } catch (error) { + console.error('Toggle contact failed:', error); + } + }, [loadContacts]); + // 组件挂载时加载展示设置 useEffect(() => { loadDisplaySettings(); loadAppAssets(); - }, [loadDisplaySettings, loadAppAssets]); + loadContacts(); + }, [loadDisplaySettings, loadAppAssets, loadContacts]); // 切换货币选择 const toggleCurrency = (currency: string) => { @@ -692,6 +770,136 @@ export default function SettingsPage() { + {/* 客服联系方式管理 */} +
+
+

客服联系方式管理

+
+ + +
+
+
+ {contacts.length === 0 && !contactsLoading && ( +

暂无客服联系方式,请点击"新增联系方式"添加

+ )} + {contacts.length > 0 && ( + + + + + + + + + + + + + {contacts.map((contact) => ( + + + + + + + + + ))} + +
排序类型标签联系方式状态操作
{contact.sortOrder}{contact.type === 'WECHAT' ? '微信' : 'QQ'}{contact.label}{contact.value} + handleContactToggle(contact.id, v)} + size="small" + /> + +
+ + +
+
+ )} + + {editingContact && ( +
+

{editingContact.id ? '编辑联系方式' : '新增联系方式'}

+
+ + setEditingContact({ ...editingContact, label: e.target.value })} + /> + setEditingContact({ ...editingContact, value: e.target.value })} + /> + setEditingContact({ ...editingContact, sortOrder: parseInt(e.target.value) || 0 })} + /> +
+
+ + +
+
+ )} + + + 提示:移动端"联系客服"弹窗将按排序顺序显示所有已启用的联系方式。修改后用户下次打开弹窗时生效。 + +
+
+ {/* 后台账号与安全 */}
diff --git a/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss b/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss index d575731a..009a25ce 100644 --- a/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss +++ b/frontend/admin-web/src/app/(dashboard)/settings/settings.module.scss @@ -1216,6 +1216,54 @@ gap: 6px; } +/* 客服联系方式管理 */ +.settings__contactTable { + width: 100%; + border-collapse: collapse; + font-size: 14px; + margin-bottom: 16px; + + th, td { + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid #e5e7eb; + } + + th { + font-weight: 600; + color: #374151; + background: #f9fafb; + font-size: 13px; + } + + td { + color: #4b5563; + } +} + +.settings__contactForm { + margin-top: 16px; + margin-bottom: 16px; + padding: 16px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #fafafa; + + h3 { + font-size: 15px; + font-weight: 600; + color: #374151; + margin-bottom: 12px; + } +} + +.settings__contactFormFields { + display: grid; + grid-template-columns: 120px 1fr 1fr 80px; + gap: 12px; + align-items: center; +} + /* 页面底部操作区 */ .settings__footer { width: 100%; diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index 744123c6..cf07e5e2 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -261,4 +261,12 @@ export const API_ENDPOINTS = { UPDATE: (id: string) => `/v1/admin/app-assets/${id}`, DELETE: (id: string) => `/v1/admin/app-assets/${id}`, }, + + // 客服联系方式管理 (admin-service) + CUSTOMER_SERVICE_CONTACTS: { + LIST: '/v1/admin/customer-service-contacts', + CREATE: '/v1/admin/customer-service-contacts', + UPDATE: (id: string) => `/v1/admin/customer-service-contacts/${id}`, + DELETE: (id: string) => `/v1/admin/customer-service-contacts/${id}`, + }, } as const; diff --git a/frontend/admin-web/src/services/customerServiceContactService.ts b/frontend/admin-web/src/services/customerServiceContactService.ts new file mode 100644 index 00000000..07c244d1 --- /dev/null +++ b/frontend/admin-web/src/services/customerServiceContactService.ts @@ -0,0 +1,55 @@ +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; + +export type ContactType = 'WECHAT' | 'QQ'; + +export interface CustomerServiceContact { + id: string; + type: ContactType; + label: string; + value: string; + sortOrder: number; + isEnabled: boolean; + createdAt: string; + updatedAt: string; +} + +export interface CreateContactPayload { + type: ContactType; + label: string; + value: string; + sortOrder: number; + isEnabled?: boolean; +} + +export interface UpdateContactPayload { + type?: ContactType; + label?: string; + value?: string; + sortOrder?: number; + isEnabled?: boolean; +} + +export const customerServiceContactService = { + /** 查询所有客服联系方式 */ + async list(): Promise { + return apiClient.get(API_ENDPOINTS.CUSTOMER_SERVICE_CONTACTS.LIST); + }, + + /** 新增客服联系方式 */ + async create(data: CreateContactPayload): Promise { + return apiClient.post(API_ENDPOINTS.CUSTOMER_SERVICE_CONTACTS.CREATE, data); + }, + + /** 更新客服联系方式 */ + async update(id: string, data: UpdateContactPayload): Promise { + return apiClient.put(API_ENDPOINTS.CUSTOMER_SERVICE_CONTACTS.UPDATE(id), data); + }, + + /** 删除客服联系方式 */ + async delete(id: string): Promise { + return apiClient.delete(API_ENDPOINTS.CUSTOMER_SERVICE_CONTACTS.DELETE(id)); + }, +}; + +export default customerServiceContactService; diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index 00eac821..ce58617f 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -13,6 +13,7 @@ import '../services/reward_service.dart'; import '../services/notification_service.dart'; import '../services/system_config_service.dart'; import '../services/app_asset_service.dart'; +import '../services/customer_service_contact_service.dart'; import '../services/leaderboard_service.dart'; import '../services/contract_signing_service.dart'; import '../services/contract_check_service.dart'; @@ -119,6 +120,13 @@ final appAssetServiceProvider = Provider((ref) { return AppAssetService(apiClient: apiClient, localStorage: localStorage); }); +// Customer Service Contact Service Provider (调用 admin-service) +final customerServiceContactServiceProvider = Provider((ref) { + final apiClient = ref.watch(apiClientProvider); + final localStorage = ref.watch(localStorageProvider); + return CustomerServiceContactService(apiClient: apiClient, localStorage: localStorage); +}); + // Leaderboard Service Provider (调用 leaderboard-service) final leaderboardServiceProvider = Provider((ref) { final apiClient = ref.watch(apiClientProvider); diff --git a/frontend/mobile-app/lib/core/services/customer_service_contact_service.dart b/frontend/mobile-app/lib/core/services/customer_service_contact_service.dart new file mode 100644 index 00000000..465507ba --- /dev/null +++ b/frontend/mobile-app/lib/core/services/customer_service_contact_service.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import '../network/api_client.dart'; +import '../storage/local_storage.dart'; +import '../storage/storage_keys.dart'; + +/// 客服联系方式数据模型 +class CustomerServiceContactData { + final String id; + final String type; // 'WECHAT' | 'QQ' + final String label; + final String value; + final int sortOrder; + + const CustomerServiceContactData({ + required this.id, + required this.type, + required this.label, + required this.value, + required this.sortOrder, + }); + + factory CustomerServiceContactData.fromJson(Map json) { + return CustomerServiceContactData( + id: json['id'] as String, + type: json['type'] as String, + label: json['label'] as String, + value: json['value'] as String, + sortOrder: json['sortOrder'] as int, + ); + } + + Map toJson() => { + 'id': id, + 'type': type, + 'label': label, + 'value': value, + 'sortOrder': sortOrder, + }; + + bool get isWechat => type == 'WECHAT'; + bool get isQQ => type == 'QQ'; +} + +/// 客服联系方式服务 +/// +/// 策略与 AppAssetService 一致: +/// 1. 优先使用 SharedPreferences 中的持久化缓存 +/// 2. 后台静默刷新,成功后更新缓存 +/// 3. 缓存为空时返回空列表 +class CustomerServiceContactService { + final ApiClient _apiClient; + final LocalStorage _localStorage; + + /// 内存缓存 + List? _memoryCache; + + /// 缓存有效期 + static const Duration _cacheExpiration = Duration(minutes: 5); + + /// 上次网络请求时间 + DateTime? _lastFetchTime; + + CustomerServiceContactService({ + required ApiClient apiClient, + required LocalStorage localStorage, + }) : _apiClient = apiClient, + _localStorage = localStorage; + + /// 获取客服联系方式列表 + /// + /// 返回顺序:内存缓存 → 持久化缓存 → 空列表 + /// 同时在后台静默刷新数据。 + Future> getContacts({bool forceRefresh = false}) async { + // 1. 检查内存缓存 + if (!forceRefresh && _memoryCache != null && _lastFetchTime != null) { + final cacheAge = DateTime.now().difference(_lastFetchTime!); + if (cacheAge < _cacheExpiration) { + return _memoryCache!; + } + } + + // 2. 从 SharedPreferences 加载 + if (_memoryCache == null) { + try { + final cachedJson = _localStorage.getString(StorageKeys.cachedCustomerServiceContacts); + if (cachedJson != null && cachedJson.isNotEmpty) { + final list = jsonDecode(cachedJson) as List; + _memoryCache = list + .map((e) => CustomerServiceContactData.fromJson(e as Map)) + .toList() + ..sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); + debugPrint('[CustomerServiceContactService] 从持久化缓存加载: ${_memoryCache!.length} contacts'); + } + } catch (e) { + debugPrint('[CustomerServiceContactService] 读取持久化缓存失败: $e'); + } + } + + // 3. 后台静默刷新 + _refreshInBackground(); + + return _memoryCache ?? []; + } + + /// 后台静默刷新 + void _refreshInBackground() { + _fetchFromApi().then((contacts) { + if (contacts != null) { + _memoryCache = contacts; + _lastFetchTime = DateTime.now(); + // 持久化 + final jsonStr = jsonEncode(contacts.map((e) => e.toJson()).toList()); + _localStorage.setString(StorageKeys.cachedCustomerServiceContacts, jsonStr); + debugPrint('[CustomerServiceContactService] 后台刷新成功: ${contacts.length} contacts'); + } + }).catchError((e) { + debugPrint('[CustomerServiceContactService] 后台刷新失败: $e'); + }); + } + + /// 从 API 获取数据 + Future?> _fetchFromApi() async { + try { + final response = await _apiClient.get('/customer-service-contacts'); + List? list; + if (response.data is List) { + list = response.data as List; + } else if (response.data is Map && response.data['data'] is List) { + list = response.data['data'] as List; + } + if (list != null) { + return list + .map((e) => CustomerServiceContactData.fromJson(e as Map)) + .toList() + ..sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); + } + return null; + } catch (e) { + debugPrint('[CustomerServiceContactService] API 请求失败: $e'); + return null; + } + } +} diff --git a/frontend/mobile-app/lib/core/storage/storage_keys.dart b/frontend/mobile-app/lib/core/storage/storage_keys.dart index 36add575..a185aac8 100644 --- a/frontend/mobile-app/lib/core/storage/storage_keys.dart +++ b/frontend/mobile-app/lib/core/storage/storage_keys.dart @@ -50,6 +50,7 @@ class StorageKeys { static const String cachedRankingData = 'cached_ranking_data'; static const String cachedMiningStatus = 'cached_mining_status'; static const String cachedAppAssets = 'cached_app_assets'; + static const String cachedCustomerServiceContacts = 'cached_customer_service_contacts'; // ===== 已废弃 (保留用于迁移旧数据) ===== @Deprecated('Use userSerialNum instead') diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index d3b9641d..73781daf 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -26,6 +26,7 @@ import '../widgets/team_tree_widget.dart'; import '../widgets/stacked_cards_widget.dart'; import '../../../authorization/presentation/widgets/stickman_race_widget.dart'; import '../../../kyc/data/kyc_service.dart'; +import '../../../../core/services/customer_service_contact_service.dart'; /// 个人中心页面 - 显示用户信息、社区数据、收益和设置 /// 包含用户资料、推荐信息、社区考核、收益领取等功能 @@ -4346,8 +4347,13 @@ class _ProfilePageState extends ConsumerState { context.push(RoutePaths.accountSwitch); } - /// 显示客服联系方式弹窗 - void _showCustomerServiceDialog() { + /// 显示客服联系方式弹窗 (从后台动态加载) + void _showCustomerServiceDialog() async { + final contactService = ref.read(customerServiceContactServiceProvider); + final contacts = await contactService.getContacts(); + + if (!mounted) return; + showDialog( context: context, builder: (context) => Dialog( @@ -4385,41 +4391,40 @@ class _ProfilePageState extends ConsumerState { ], ), const SizedBox(height: 24), - // 微信客服1 - _buildContactItem( - icon: 'assets/icons/wechat_icon.png', - iconFallback: Icons.message, - label: '客服微信1', - value: 'liulianhuanghou1', - onCopy: () => _copyToClipboard('liulianhuanghou1', '微信号'), - ), - const SizedBox(height: 12), - // 微信客服2 - _buildContactItem( - icon: 'assets/icons/wechat_icon.png', - iconFallback: Icons.message, - label: '客服微信2', - value: 'liulianhuanghou2', - onCopy: () => _copyToClipboard('liulianhuanghou2', '微信号'), - ), - const SizedBox(height: 12), - // QQ客服1 - _buildContactItem( - icon: 'assets/icons/qq_icon.png', - iconFallback: Icons.chat_bubble, - label: '客服QQ1', - value: '1502109619', - onCopy: () => _copyToClipboard('1502109619', 'QQ号'), - ), - const SizedBox(height: 12), - // QQ客服2 - _buildContactItem( - icon: 'assets/icons/qq_icon.png', - iconFallback: Icons.chat_bubble, - label: '客服QQ2', - value: '2171447109', - onCopy: () => _copyToClipboard('2171447109', 'QQ号'), - ), + // 动态联系方式列表 + if (contacts.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text( + '暂无客服联系方式', + style: TextStyle( + fontSize: 14, + color: Color(0xFF8B5A2B), + ), + ), + ) + else + ...contacts.asMap().entries.map((entry) { + final index = entry.key; + final contact = entry.value; + final copyLabel = contact.isWechat ? '微信号' : 'QQ号'; + return Column( + children: [ + if (index > 0) const SizedBox(height: 12), + _buildContactItem( + icon: contact.isWechat + ? 'assets/icons/wechat_icon.png' + : 'assets/icons/qq_icon.png', + iconFallback: contact.isWechat + ? Icons.message + : Icons.chat_bubble, + label: contact.label, + value: contact.value, + onCopy: () => _copyToClipboard(contact.value, copyLabel), + ), + ], + ); + }), const SizedBox(height: 24), // 关闭按钮 SizedBox(