From a284fbe3f311c12442a67c7267cd967c892e7f10 Mon Sep 17 00:00:00 2001 From: inman Date: Thu, 23 Jul 2026 18:44:54 +0800 Subject: [PATCH] remove unused WeCom backend integration --- .env.example | 12 - apps/api/prisma/seed.ts | 9 - apps/api/src/routes/admin.ts | 252 ------------------ apps/api/src/routes/memory.ts | 256 ------------------- apps/api/src/routes/public.ts | 16 -- apps/api/src/routes/wecom.ts | 168 ------------ apps/api/src/schemas.ts | 23 -- apps/api/src/server.ts | 6 - apps/api/src/wecom.ts | 228 ----------------- apps/miniprogram/README.md | 4 +- apps/miniprogram/src/pages/contact/index.tsx | 54 +--- 11 files changed, 6 insertions(+), 1022 deletions(-) delete mode 100644 apps/api/src/routes/wecom.ts delete mode 100644 apps/api/src/wecom.ts diff --git a/.env.example b/.env.example index 1f2253f..f31f81e 100644 --- a/.env.example +++ b/.env.example @@ -3,18 +3,6 @@ DATABASE_URL="postgresql://miniapp:miniapp_dev_password@localhost:5433/miniapp?s JWT_SECRET="replace-with-a-long-random-secret-before-production" WECHAT_APP_ID="your-wechat-mini-program-app-id" WECHAT_APP_SECRET="your-wechat-mini-program-app-secret" -WECOM_CORP_ID="ww_your_corp_id" -WECOM_CORP_SECRET="your-wecom-third-party-app-secret" -WECOM_CONTACT_WAY_CONFIG_ID="5f8a2d2756e2c69776c186ef28fc48ee" -WECOM_CONTACT_WAY_USER_ID="your-wecom-service-manager-userid" -WECOM_CONTACT_WAY_TYPE="1" -WECOM_CONTACT_WAY_SCENE="1" -WECOM_CONTACT_WAY_STYLE="1" -WECOM_CONTACT_WAY_REMARK="" -WECOM_CONTACT_WAY_STATE="" -WECOM_CONTACT_WAY_ENABLED="1" -WECOM_CALLBACK_TOKEN="your-wecom-callback-token" -WECOM_CALLBACK_AES_KEY="your-wecom-callback-aes-key-without-equals" MINIAPP_MOCK_AUTH="0" PORT=4000 MEDIA_PUBLIC_BASE_URL="" diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts index fdb55c9..da77397 100644 --- a/apps/api/prisma/seed.ts +++ b/apps/api/prisma/seed.ts @@ -9,7 +9,6 @@ import { createHomeModuleSnapshot, DEFAULT_HOME_MODULES } from "../src/home-modu import { collectImageUrls, materializeImageUrls } from "../src/media-migration.ts"; import { mediaStorage } from "../src/media-storage.ts"; import { DEFAULT_SEARCH_PAGE_CONFIG, SEARCH_PAGE_CONFIG_ID } from "../src/search-page.ts"; -import { getWecomContactWayConfigDefaults } from "../src/wecom.ts"; type SourceProduct = { id: number; @@ -115,7 +114,6 @@ async function main() { const seededSearchPage = await materializeImageUrls(DEFAULT_SEARCH_PAGE_CONFIG); await prisma.auditLog.deleteMany(); - await prisma.wecomExternalContact.deleteMany(); await prisma.siteVersion.deleteMany(); await prisma.homeModule.deleteMany(); await prisma.searchPageConfig.deleteMany(); @@ -137,13 +135,6 @@ async function main() { await prisma.destination.deleteMany(); await prisma.mediaAsset.deleteMany(); - const wecomDefaults = getWecomContactWayConfigDefaults(); - await prisma.wecomContactWay.upsert({ - where: { configId: wecomDefaults.configId }, - update: {}, - create: wecomDefaults, - }); - await prisma.homeModule.createMany({ data: seededHomeModules.map((module) => ({ ...module, diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index c105d02..1292a2e 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -38,19 +38,7 @@ import { productUpdateSchema, searchPageConfigSchema, storedImageUrlSchema, - wecomContactLinkSchema, - wecomContactQuerySchema, - wecomContactWayPatchSchema, } from "../schemas.js"; -import { - getWecomContactWay, - getWecomContactWayConfigDefaults, - getWecomExternalContact, - hasWecomApiCredentials, - hasWecomCallbackCredentials, - WECOM_CONTACT_PLUGIN_ID, - WECOM_CONTACT_PLUGIN_VERSION, -} from "../wecom.js"; type SourceProduct = { id: number; @@ -495,39 +483,6 @@ async function audit(actorId: string | undefined, action: string, entity: string }); } -function serializeWecomContactWay(config: { - id?: string | null; - configId: string; - type: number; - scene: number; - style: number; - remark: string | null; - state: string | null; - wecomUserId: string | null; - enabled: boolean; - lastSyncedAt?: Date | null; - lastSyncError?: string | null; -}) { - return { - id: config.id ?? null, - configId: config.configId, - type: config.type, - scene: config.scene, - style: config.style, - remark: config.remark, - state: config.state, - wecomUserId: config.wecomUserId, - enabled: config.enabled, - lastSyncedAt: config.lastSyncedAt ?? null, - lastSyncError: config.lastSyncError ?? null, - apiCredentialsConfigured: hasWecomApiCredentials(), - callbackCredentialsConfigured: hasWecomCallbackCredentials(), - pluginId: WECOM_CONTACT_PLUGIN_ID, - pluginVersion: WECOM_CONTACT_PLUGIN_VERSION, - callbackPath: "/api/integrations/wecom/callback", - }; -} - export async function registerAdminRoutes(app: FastifyInstance) { app.post("/api/admin/auth/login", async (request, reply) => { const body = loginSchema.parse(request.body); @@ -1137,213 +1092,6 @@ export async function registerAdminRoutes(app: FastifyInstance) { return { ...lead, phone: lead.phone }; }); - app.get("/api/admin/wecom/contact-way", { preHandler: requireAdmin }, async () => { - const defaults = getWecomContactWayConfigDefaults(); - const stored = await prisma.wecomContactWay.findFirst({ orderBy: { updatedAt: "desc" } }); - return serializeWecomContactWay(stored ?? defaults); - }); - - app.patch("/api/admin/wecom/contact-way", { preHandler: requireAdmin }, async (request) => { - const body = wecomContactWayPatchSchema.parse(request.body); - const defaults = getWecomContactWayConfigDefaults(); - const before = await prisma.wecomContactWay.findFirst({ orderBy: { updatedAt: "desc" } }); - const saved = before - ? await prisma.wecomContactWay.update({ - where: { id: before.id }, - data: { - configId: body.configId, - type: body.type ?? before.type, - scene: body.scene ?? before.scene, - style: body.style ?? before.style, - remark: body.remark === undefined ? before.remark : body.remark, - state: body.state === undefined ? before.state : body.state, - wecomUserId: body.wecomUserId === undefined ? before.wecomUserId : body.wecomUserId, - enabled: body.enabled ?? before.enabled, - lastSyncError: null, - }, - }) - : await prisma.wecomContactWay.create({ - data: { - ...defaults, - ...body, - type: body.type ?? defaults.type, - scene: body.scene ?? defaults.scene, - style: body.style ?? defaults.style, - remark: body.remark === undefined ? defaults.remark : body.remark, - state: body.state === undefined ? defaults.state : body.state, - wecomUserId: body.wecomUserId === undefined ? defaults.wecomUserId : body.wecomUserId, - enabled: body.enabled ?? defaults.enabled, - }, - }); - const response = serializeWecomContactWay(saved); - await audit(getActorId(request), "update", "wecom_contact_way", saved.id, response, before); - return response; - }); - - app.post("/api/admin/wecom/contact-way/sync", { preHandler: requireAdmin }, async (request, reply) => { - const defaults = getWecomContactWayConfigDefaults(); - const current = await prisma.wecomContactWay.findFirst({ orderBy: { updatedAt: "desc" } }); - const local = current ?? await prisma.wecomContactWay.create({ data: defaults }); - if (!hasWecomApiCredentials()) return reply.status(503).send({ message: "请先配置企微服务端凭证" }); - try { - const remote = await getWecomContactWay(local.configId); - const remoteConfig = remote.config ?? {}; - const saved = await prisma.wecomContactWay.update({ - where: { id: local.id }, - data: { - configId: remoteConfig.config_id ?? local.configId, - type: remoteConfig.type ?? local.type, - scene: remoteConfig.scene ?? local.scene, - style: remoteConfig.style ?? local.style, - remark: remoteConfig.remark ?? local.remark, - state: remoteConfig.state ?? local.state, - wecomUserId: remoteConfig.user ?? local.wecomUserId, - lastSyncedAt: new Date(), - lastSyncError: null, - }, - }); - const response = serializeWecomContactWay(saved); - await audit(getActorId(request), "sync", "wecom_contact_way", saved.id, response, local); - return response; - } catch (error) { - const message = error instanceof Error ? error.message : "企微配置同步失败"; - await prisma.wecomContactWay.update({ where: { id: local.id }, data: { lastSyncError: message } }); - return reply.status(502).send({ message }); - } - }); - - app.get("/api/admin/wecom/contacts", { preHandler: requireAdmin }, async (request) => { - const query = wecomContactQuerySchema.parse(request.query); - const where = { - status: query.status || undefined, - OR: query.keyword - ? [ - { name: { contains: query.keyword, mode: "insensitive" as const } }, - { externalUserId: { contains: query.keyword } }, - { wecomUserId: { contains: query.keyword } }, - { remark: { contains: query.keyword, mode: "insensitive" as const } }, - { corpName: { contains: query.keyword, mode: "insensitive" as const } }, - ] - : undefined, - }; - const [contacts, total] = await Promise.all([ - prisma.wecomExternalContact.findMany({ - where, - include: { - _count: { select: { leads: true } }, - contactWay: { select: { id: true, configId: true, style: true } }, - miniProgramUser: { select: { id: true, nickname: true, phone: true } }, - customer: { select: { id: true, name: true, phone: true } }, - }, - orderBy: { lastEventAt: "desc" }, - take: query.take, - skip: query.skip, - }), - prisma.wecomExternalContact.count({ where }), - ]); - return { - items: contacts.map((contact) => ({ - ...contact, - miniProgramUser: contact.miniProgramUser - ? { ...contact.miniProgramUser, phoneMasked: maskPhone(contact.miniProgramUser.phone) } - : null, - customer: contact.customer ? { ...contact.customer, phoneMasked: maskPhone(contact.customer.phone) } : null, - leadCount: contact._count.leads, - })), - total, - take: query.take, - skip: query.skip, - }; - }); - - app.get("/api/admin/wecom/contacts/:id", { preHandler: requireAdmin }, async (request, reply) => { - const { id } = z.object({ id: z.string().uuid() }).parse(request.params); - const contact = await prisma.wecomExternalContact.findUnique({ - where: { id }, - include: { - contactWay: true, - miniProgramUser: { select: { id: true, nickname: true, phone: true, customerId: true } }, - customer: true, - leads: { select: { id: true, status: true, destination: true, contactName: true, createdAt: true } }, - }, - }); - if (!contact) return reply.status(404).send({ message: "企微客户不存在" }); - return { - ...contact, - miniProgramUser: contact.miniProgramUser - ? { ...contact.miniProgramUser, phoneMasked: maskPhone(contact.miniProgramUser.phone) } - : null, - customer: contact.customer ? { ...contact.customer, phoneMasked: maskPhone(contact.customer.phone) } : null, - }; - }); - - app.patch("/api/admin/wecom/contacts/:id/link", { preHandler: requireAdmin }, async (request, reply) => { - const { id } = z.object({ id: z.string().uuid() }).parse(request.params); - const body = wecomContactLinkSchema.parse(request.body); - const before = await prisma.wecomExternalContact.findUnique({ where: { id } }); - if (!before) return reply.status(404).send({ message: "企微客户不存在" }); - - const saved = await prisma.$transaction(async (transaction) => { - let customerId: string | null | undefined; - if (body.miniProgramUserId !== undefined) { - const user = body.miniProgramUserId - ? await transaction.miniProgramUser.findUnique({ where: { id: body.miniProgramUserId }, select: { id: true, customerId: true } }) - : null; - if (body.miniProgramUserId && !user) throw new Error("小程序用户不存在"); - customerId = user?.customerId ?? null; - } - if (body.leadId !== undefined) { - if (body.leadId) { - const lead = await transaction.lead.findUnique({ where: { id: body.leadId }, select: { id: true } }); - if (!lead) throw new Error("需求线索不存在"); - } - await transaction.lead.updateMany({ where: { wecomContactId: id }, data: { wecomContactId: null } }); - if (body.leadId) await transaction.lead.update({ where: { id: body.leadId }, data: { wecomContactId: id } }); - } - return transaction.wecomExternalContact.update({ - where: { id }, - data: { - miniProgramUserId: body.miniProgramUserId === undefined ? undefined : body.miniProgramUserId, - customerId: body.miniProgramUserId === undefined ? undefined : customerId, - }, - }); - }); - await audit(getActorId(request), "link", "wecom_external_contact", saved.id, saved, before); - return { id: saved.id, miniProgramUserId: saved.miniProgramUserId, customerId: saved.customerId, leadId: body.leadId ?? undefined }; - }); - - app.post("/api/admin/wecom/contacts/:id/refresh", { preHandler: requireAdmin }, async (request, reply) => { - const { id } = z.object({ id: z.string().uuid() }).parse(request.params); - const contact = await prisma.wecomExternalContact.findUnique({ where: { id } }); - if (!contact) return reply.status(404).send({ message: "企微客户不存在" }); - if (!hasWecomApiCredentials()) return reply.status(503).send({ message: "请先配置企微服务端凭证" }); - try { - const detail = await getWecomExternalContact(contact.externalUserId, contact.wecomUserId); - const external = detail.external_contact; - const follow = detail.follow_info; - const saved = await prisma.wecomExternalContact.update({ - where: { id }, - data: { - name: external?.name ?? contact.name, - avatarUrl: external?.avatar ?? contact.avatarUrl, - type: external?.type ?? contact.type, - gender: external?.gender ?? contact.gender, - unionId: external?.unionid ?? contact.unionId, - corpName: external?.corp_name ?? contact.corpName, - position: external?.position ?? contact.position, - remark: follow?.remark ?? contact.remark, - description: follow?.description ?? contact.description, - profile: external?.external_profile ? (external.external_profile as Prisma.InputJsonValue) : contact.profile ?? undefined, - lastEventAt: new Date(), - }, - }); - await audit(getActorId(request), "refresh", "wecom_external_contact", saved.id, saved, contact); - return saved; - } catch (error) { - return reply.status(502).send({ message: error instanceof Error ? error.message : "企微客户同步失败" }); - } - }); - app.get("/api/admin/media-assets", { preHandler: requireAdmin }, async () => { const assets = await prisma.mediaAsset.findMany({ orderBy: { createdAt: "desc" }, take: 200 }); return { items: assets }; diff --git a/apps/api/src/routes/memory.ts b/apps/api/src/routes/memory.ts index f257207..0b17c22 100644 --- a/apps/api/src/routes/memory.ts +++ b/apps/api/src/routes/memory.ts @@ -47,20 +47,7 @@ import { productUpdateSchema, searchPageConfigSchema, storedImageUrlSchema, - wecomContactQuerySchema, - wecomContactWayPatchSchema, } from "../schemas.js"; -import { - decryptWecomCallback, - getWecomContactWayConfigDefaults, - hasWecomApiCredentials, - hasWecomCallbackCredentials, - normalizeWecomCallbackEvent, - parseWecomXml, - verifyWecomCallback, - WECOM_CONTACT_PLUGIN_ID, - WECOM_CONTACT_PLUGIN_VERSION, -} from "../wecom.js"; type MemoryHomeModule = { id: string; @@ -170,7 +157,6 @@ type MemoryLead = { id: string; requestType: "custom" | "booking"; userId?: string | null; - wecomContactId?: string | null; destination?: string | null; phone: string; contactName?: string | null; @@ -236,47 +222,6 @@ type MemoryHistory = { lastViewedAt: string; }; -type MemoryWecomContactWay = { - id: string; - configId: string; - type: number; - scene: number; - style: number; - remark: string | null; - state: string | null; - wecomUserId: string | null; - enabled: boolean; - lastSyncedAt: string | null; - lastSyncError: string | null; - updatedAt: string; -}; - -type MemoryWecomContact = { - id: string; - externalUserId: string; - wecomUserId: string; - contactWayId: string | null; - miniProgramUserId: string | null; - customerId: string | null; - name: string | null; - avatarUrl: string | null; - type: number | null; - gender: number | null; - unionId: string | null; - corpName: string | null; - position: string | null; - remark: string | null; - description: string | null; - profile: Record | null; - status: string; - addedAt: string | null; - lastEventAt: string; - deletedAt: string | null; - createdAt: string; - updatedAt: string; - leadIds: string[]; -}; - type SourceProduct = { id: number; title: string; @@ -519,14 +464,6 @@ const mediaAssets: Array<{ id: string; url: string; name: string; mimeType: stri const miniProgramUsers: MemoryMiniProgramUser[] = []; const memoryFavorites: MemoryFavorite[] = []; const memoryHistories: MemoryHistory[] = []; -const memoryWecomContactWay: MemoryWecomContactWay = { - id: "wecom-contact-way-memory", - ...getWecomContactWayConfigDefaults(), - lastSyncedAt: null, - lastSyncError: null, - updatedAt: new Date().toISOString(), -}; -const memoryWecomContacts: MemoryWecomContact[] = []; function memoryMaskPhone(phone?: string | null) { if (!phone) return null; @@ -560,31 +497,6 @@ function serializeMemoryUser(user: MemoryMiniProgramUser) { }; } -function serializeMemoryWecomContactWay() { - return { - ...memoryWecomContactWay, - apiCredentialsConfigured: hasWecomApiCredentials(), - callbackCredentialsConfigured: hasWecomCallbackCredentials(), - pluginId: WECOM_CONTACT_PLUGIN_ID, - pluginVersion: WECOM_CONTACT_PLUGIN_VERSION, - callbackPath: "/api/integrations/wecom/callback", - }; -} - -function serializeMemoryWecomContact(contact: MemoryWecomContact) { - const user = contact.miniProgramUserId ? miniProgramUsers.find((item) => item.id === contact.miniProgramUserId) : undefined; - const linkedLeads = leads.filter((lead) => contact.leadIds.includes(lead.id) || lead.wecomContactId === contact.id); - return { - ...contact, - miniProgramUser: user - ? { id: user.id, nickname: user.nickname ?? null, phoneMasked: memoryMaskPhone(user.phone) } - : null, - customer: contact.customerId ? { id: contact.customerId, name: user?.nickname ?? null, phoneMasked: memoryMaskPhone(user?.phone) } : null, - leadCount: linkedLeads.length, - leads: linkedLeads.map((lead) => ({ id: lead.id, status: lead.status, destination: lead.destination, contactName: lead.contactName, createdAt: lead.createdAt })), - }; -} - function findMemoryProduct(id: string) { return products.find((product) => product.id === id || String(product.sourceId) === id); } @@ -1200,83 +1112,6 @@ export async function registerMemoryRoutes(app: FastifyInstance) { return { ...lead, phone: lead.phone }; }); - app.get("/api/admin/wecom/contact-way", async () => serializeMemoryWecomContactWay()); - - app.patch("/api/admin/wecom/contact-way", async (request) => { - const body = wecomContactWayPatchSchema.parse(request.body); - memoryWecomContactWay.configId = body.configId; - memoryWecomContactWay.type = body.type ?? memoryWecomContactWay.type; - memoryWecomContactWay.scene = body.scene ?? memoryWecomContactWay.scene; - memoryWecomContactWay.style = body.style ?? memoryWecomContactWay.style; - if (body.remark !== undefined) memoryWecomContactWay.remark = body.remark; - if (body.state !== undefined) memoryWecomContactWay.state = body.state; - if (body.wecomUserId !== undefined) memoryWecomContactWay.wecomUserId = body.wecomUserId; - memoryWecomContactWay.enabled = body.enabled ?? memoryWecomContactWay.enabled; - memoryWecomContactWay.lastSyncError = null; - memoryWecomContactWay.updatedAt = new Date().toISOString(); - return serializeMemoryWecomContactWay(); - }); - - app.post("/api/admin/wecom/contact-way/sync", async (_request, reply) => { - memoryWecomContactWay.lastSyncError = "内存开发库不连接企微远程接口"; - return reply.status(503).send({ message: memoryWecomContactWay.lastSyncError }); - }); - - app.get("/api/admin/wecom/contacts", async (request) => { - const query = wecomContactQuerySchema.parse(request.query); - const keyword = query.keyword?.toLowerCase(); - const matched = memoryWecomContacts.filter((contact) => { - if (query.status && contact.status !== query.status) return false; - if (!keyword) return true; - return [contact.name, contact.externalUserId, contact.wecomUserId, contact.remark, contact.corpName].filter(Boolean).join(" ").toLowerCase().includes(keyword); - }); - return { - items: matched.slice(query.skip, query.skip + query.take).map(serializeMemoryWecomContact), - total: matched.length, - take: query.take, - skip: query.skip, - }; - }); - - app.get("/api/admin/wecom/contacts/:id", async (request, reply) => { - const { id } = z.object({ id: z.string().min(1) }).parse(request.params); - const contact = memoryWecomContacts.find((item) => item.id === id); - if (!contact) return reply.status(404).send({ message: "企微客户不存在" }); - return serializeMemoryWecomContact(contact); - }); - - app.patch("/api/admin/wecom/contacts/:id/link", async (request, reply) => { - const { id } = z.object({ id: z.string().min(1) }).parse(request.params); - const body = z.object({ - miniProgramUserId: z.string().min(1).nullable().optional(), - leadId: z.string().min(1).nullable().optional(), - }).parse(request.body); - const contact = memoryWecomContacts.find((item) => item.id === id); - if (!contact) return reply.status(404).send({ message: "企微客户不存在" }); - if (body.miniProgramUserId !== undefined) { - if (body.miniProgramUserId && !miniProgramUsers.some((user) => user.id === body.miniProgramUserId)) return reply.status(404).send({ message: "小程序用户不存在" }); - contact.miniProgramUserId = body.miniProgramUserId; - contact.customerId = body.miniProgramUserId ? `memory-customer-${body.miniProgramUserId}` : null; - } - if (body.leadId !== undefined) { - contact.leadIds = body.leadId ? [body.leadId] : []; - leads.forEach((lead) => { - if (lead.wecomContactId === contact.id) lead.wecomContactId = null; - }); - if (body.leadId) { - const lead = leads.find((item) => item.id === body.leadId); - if (!lead) return reply.status(404).send({ message: "需求线索不存在" }); - lead.wecomContactId = contact.id; - } - } - contact.updatedAt = new Date().toISOString(); - return serializeMemoryWecomContact(contact); - }); - - app.post("/api/admin/wecom/contacts/:id/refresh", async (_request, reply) => { - return reply.status(503).send({ message: "内存开发库不连接企微远程接口" }); - }); - app.post("/api/public/auth/wechat-login", async (request, reply) => { const body = miniProgramLoginSchema.parse(request.body); const now = new Date().toISOString(); @@ -1454,20 +1289,6 @@ export async function registerMemoryRoutes(app: FastifyInstance) { })); }); - app.get("/api/public/wecom/contact-way", async () => { - const config = getWecomContactWayConfigDefaults(); - return { - enabled: config.enabled, - configId: config.configId, - type: config.type, - scene: config.scene, - style: config.style, - remark: config.remark, - state: config.state, - pluginId: WECOM_CONTACT_PLUGIN_ID, - pluginVersion: WECOM_CONTACT_PLUGIN_VERSION, - }; - }); app.get("/api/public/site-config", async () => siteConfig(true)); app.get("/api/public/products", async () => ({ items: products.filter((product) => product.status === "published").map(productWithDestination) })); app.get("/api/public/products/:id", async (request, reply) => { @@ -1536,81 +1357,4 @@ export async function registerMemoryRoutes(app: FastifyInstance) { return reply.status(201).send({ id: lead.id, status: lead.status }); }); - app.get("/api/integrations/wecom/callback", async (request, reply) => { - const query = (request.query ?? {}) as Record; - const signature = String(query.msg_signature ?? query.signature ?? ""); - const timestamp = String(query.timestamp ?? ""); - const nonce = String(query.nonce ?? ""); - const echo = String(query.echostr ?? ""); - if (!hasWecomCallbackCredentials()) return reply.status(503).send({ message: "企微回调尚未配置" }); - if (!verifyWecomCallback(signature, timestamp, nonce, echo)) return reply.status(401).send({ message: "企微回调签名校验失败" }); - try { - return reply.type("text/plain").send(decryptWecomCallback(echo)); - } catch { - return reply.status(400).send({ message: "企微回调地址校验失败" }); - } - }); - - app.post("/api/integrations/wecom/callback", async (request, reply) => { - const query = (request.query ?? {}) as Record; - const signature = String(query.msg_signature ?? query.signature ?? ""); - const timestamp = String(query.timestamp ?? ""); - const nonce = String(query.nonce ?? ""); - const outerXml = typeof request.body === "string" ? request.body : ""; - const encrypted = parseWecomXml(outerXml).Encrypt; - if (!hasWecomCallbackCredentials()) return reply.status(503).send({ message: "企微回调尚未配置" }); - if (!encrypted || !verifyWecomCallback(signature, timestamp, nonce, encrypted)) return reply.status(401).send({ message: "企微回调签名校验失败" }); - try { - const event = normalizeWecomCallbackEvent(decryptWecomCallback(encrypted)); - if (event.externalUserId && event.userId) { - const now = event.eventTime.toISOString(); - const existing = memoryWecomContacts.find((item) => item.externalUserId === event.externalUserId && item.wecomUserId === event.userId); - if (event.changeType === "del_external_contact" || event.changeType === "del_follow_user") { - if (existing) { - existing.status = "deleted"; - existing.deletedAt = now; - existing.lastEventAt = now; - existing.updatedAt = now; - } - } else if (event.changeType === "add_external_contact" || event.changeType === "edit_external_contact") { - memoryWecomContactWay.wecomUserId ??= event.userId; - if (existing) { - existing.status = "active"; - existing.deletedAt = null; - existing.lastEventAt = now; - existing.updatedAt = now; - } else { - memoryWecomContacts.unshift({ - id: randomUUID(), - externalUserId: event.externalUserId, - wecomUserId: event.userId, - contactWayId: memoryWecomContactWay.id, - miniProgramUserId: null, - customerId: null, - name: null, - avatarUrl: null, - type: null, - gender: null, - unionId: null, - corpName: null, - position: null, - remark: null, - description: null, - profile: null, - status: "active", - addedAt: now, - lastEventAt: now, - deletedAt: null, - createdAt: now, - updatedAt: now, - leadIds: [], - }); - } - } - } - return reply.type("text/plain").send("success"); - } catch { - return reply.status(500).send({ message: "企微回调处理失败" }); - } - }); } diff --git a/apps/api/src/routes/public.ts b/apps/api/src/routes/public.ts index ca74c68..1767994 100644 --- a/apps/api/src/routes/public.ts +++ b/apps/api/src/routes/public.ts @@ -15,7 +15,6 @@ import { miniProgramProfileUpdateSchema, } from "../schemas.js"; import { exchangeWechatLoginCode, exchangeWechatPhoneCode, getWechatAppId } from "../wechat.js"; -import { getWecomContactWayConfigDefaults, WECOM_CONTACT_PLUGIN_ID, WECOM_CONTACT_PLUGIN_VERSION } from "../wecom.js"; const productQuerySchema = z.object({ keyword: z.string().optional(), @@ -365,21 +364,6 @@ export async function registerPublicRoutes(app: FastifyInstance) { app.get("/health", async () => ({ ok: true, service: "miniapp-api", mode: "postgres", persistent: true })); - app.get("/api/public/wecom/contact-way", async () => { - const config = getWecomContactWayConfigDefaults(); - return { - enabled: config.enabled, - configId: config.configId, - type: config.type, - scene: config.scene, - style: config.style, - remark: config.remark, - state: config.state, - pluginId: WECOM_CONTACT_PLUGIN_ID, - pluginVersion: WECOM_CONTACT_PLUGIN_VERSION, - }; - }); - app.get("/api/public/site-config", async () => { const [homeModules, heroSlides, destinations, themes, ctaBanners, campaigns, destinationPageConfig, searchPageConfig] = await Promise.all([ listHomeModules(true), diff --git a/apps/api/src/routes/wecom.ts b/apps/api/src/routes/wecom.ts deleted file mode 100644 index f58d853..0000000 --- a/apps/api/src/routes/wecom.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Prisma } from "@prisma/client"; -import type { FastifyInstance } from "fastify"; -import { prisma } from "../prisma.js"; -import { - decryptWecomCallback, - getWecomContactWayConfigDefaults, - getWecomExternalContact, - hasWecomApiCredentials, - hasWecomCallbackCredentials, - normalizeWecomCallbackEvent, - parseWecomXml, - verifyWecomCallback, -} from "../wecom.js"; - -function stringValue(value: unknown) { - return typeof value === "string" ? value : String(value ?? ""); -} - -function callbackQuery(request: { query: unknown }) { - const query = (request.query ?? {}) as Record; - return { - signature: stringValue(query.msg_signature ?? query.signature), - timestamp: stringValue(query.timestamp), - nonce: stringValue(query.nonce), - echo: stringValue(query.echostr), - }; -} - -function callbackXml(request: { body: unknown }) { - return typeof request.body === "string" ? request.body : ""; -} - -async function ensureContactWay(eventUserId: string | null) { - const defaults = getWecomContactWayConfigDefaults(); - return prisma.wecomContactWay.upsert({ - where: { configId: defaults.configId }, - update: { - wecomUserId: defaults.wecomUserId ?? eventUserId ?? undefined, - }, - create: { - ...defaults, - wecomUserId: eventUserId ?? defaults.wecomUserId, - }, - }); -} - -async function consumeWecomEvent(app: FastifyInstance, event: ReturnType) { - if (!event.externalUserId || !event.userId) return { handled: false, reason: "missing_external_contact_identity" }; - const changeType = event.changeType; - const existing = await prisma.wecomExternalContact.findUnique({ - where: { externalUserId_wecomUserId: { externalUserId: event.externalUserId, wecomUserId: event.userId } }, - }); - - if (changeType === "del_external_contact" || changeType === "del_follow_user") { - if (!existing) return { handled: true, status: "deleted", created: false }; - const deleted = await prisma.wecomExternalContact.update({ - where: { id: existing.id }, - data: { status: "deleted", deletedAt: event.eventTime, lastEventAt: event.eventTime }, - }); - return { handled: true, status: deleted.status, id: deleted.id, created: false }; - } - - if (changeType !== "add_external_contact" && changeType !== "edit_external_contact") { - return { handled: false, reason: `unsupported_change_type:${changeType ?? "unknown"}` }; - } - - const contactWay = await ensureContactWay(event.userId); - let detail: Awaited> | null = null; - if (hasWecomApiCredentials()) { - try { - detail = await getWecomExternalContact(event.externalUserId, event.userId); - } catch (error) { - app.log.warn({ error }, "企微外部联系人详情获取失败,将保留回调基础信息"); - } - } - - const external = detail?.external_contact; - const follow = detail?.follow_info; - const unionId = external?.unionid ?? null; - const linkedUser = unionId - ? await prisma.miniProgramUser.findFirst({ where: { unionId }, select: { id: true, customerId: true } }) - : null; - const profile = external?.external_profile; - const profileJson = profile ? (profile as Prisma.InputJsonValue) : undefined; - const contact = await prisma.wecomExternalContact.upsert({ - where: { externalUserId_wecomUserId: { externalUserId: event.externalUserId, wecomUserId: event.userId } }, - update: { - contactWayId: contactWay.id, - name: external?.name ?? existing?.name ?? null, - avatarUrl: external?.avatar ?? existing?.avatarUrl ?? null, - type: external?.type ?? existing?.type ?? null, - gender: external?.gender ?? existing?.gender ?? null, - unionId: unionId ?? existing?.unionId ?? null, - corpName: external?.corp_name ?? existing?.corpName ?? null, - position: external?.position ?? existing?.position ?? null, - remark: follow?.remark ?? existing?.remark ?? null, - description: follow?.description ?? existing?.description ?? null, - profile: profileJson ?? existing?.profile ?? undefined, - status: "active", - addedAt: follow?.createtime ? new Date(follow.createtime * 1000) : existing?.addedAt ?? event.eventTime, - deletedAt: null, - lastEventAt: event.eventTime, - miniProgramUserId: linkedUser?.id ?? existing?.miniProgramUserId ?? null, - customerId: linkedUser?.customerId ?? existing?.customerId ?? null, - }, - create: { - externalUserId: event.externalUserId, - wecomUserId: event.userId, - contactWayId: contactWay.id, - name: external?.name ?? null, - avatarUrl: external?.avatar ?? null, - type: external?.type ?? null, - gender: external?.gender ?? null, - unionId, - corpName: external?.corp_name ?? null, - position: external?.position ?? null, - remark: follow?.remark ?? null, - description: follow?.description ?? null, - profile: profileJson, - status: "active", - addedAt: follow?.createtime ? new Date(follow.createtime * 1000) : event.eventTime, - lastEventAt: event.eventTime, - miniProgramUserId: linkedUser?.id ?? null, - customerId: linkedUser?.customerId ?? null, - }, - }); - return { handled: true, status: contact.status, id: contact.id, created: !existing, linkedUserId: contact.miniProgramUserId }; -} - -export async function registerWecomRoutes(app: FastifyInstance) { - app.get("/api/integrations/wecom/callback", async (request, reply) => { - const query = callbackQuery(request); - if (!hasWecomCallbackCredentials()) return reply.status(503).send({ message: "企微回调尚未配置" }); - if (!verifyWecomCallback(query.signature, query.timestamp, query.nonce, query.echo)) return reply.status(401).send({ message: "企微回调签名校验失败" }); - try { - const plaintext = decryptWecomCallback(query.echo); - return reply.type("text/plain").send(plaintext); - } catch (error) { - app.log.warn({ error }, "企微回调地址校验解密失败"); - return reply.status(400).send({ message: "企微回调地址校验失败" }); - } - }); - - app.post("/api/integrations/wecom/callback", async (request, reply) => { - const query = callbackQuery(request); - const outerXml = callbackXml(request); - if (!hasWecomCallbackCredentials()) return reply.status(503).send({ message: "企微回调尚未配置" }); - const outer = parseWecomXml(outerXml); - const encrypted = outer.Encrypt; - if (!encrypted || !verifyWecomCallback(query.signature, query.timestamp, query.nonce, encrypted)) { - return reply.status(401).send({ message: "企微回调签名校验失败" }); - } - - try { - const event = normalizeWecomCallbackEvent(decryptWecomCallback(encrypted)); - const result = await consumeWecomEvent(app, event); - app.log.info({ infoType: event.infoType, changeType: event.changeType, result }, "企微客户联系回调已处理"); - return reply.type("text/plain").send("success"); - } catch (error) { - app.log.error({ error }, "企微客户联系回调处理失败"); - return reply.status(500).send({ message: "企微回调处理失败" }); - } - }); -} - -export async function syncWecomContactEventForTests(app: FastifyInstance, xml: string) { - return consumeWecomEvent(app, normalizeWecomCallbackEvent(xml)); -} diff --git a/apps/api/src/schemas.ts b/apps/api/src/schemas.ts index 357cef1..a14936f 100644 --- a/apps/api/src/schemas.ts +++ b/apps/api/src/schemas.ts @@ -248,26 +248,3 @@ export const searchPageConfigSchema = z.object({ popularKeywords: z.array(searchPageKeywordSchema).max(12), groups: z.array(searchPageGroupSchema).max(12), }); - -export const wecomContactWayPatchSchema = z.object({ - configId: z.string().trim().min(1).max(64), - type: z.number().int().min(1).max(2).optional(), - scene: z.number().int().min(1).max(2).optional(), - style: z.number().int().min(1).max(3).optional(), - remark: z.string().trim().max(30).nullable().optional(), - state: z.string().trim().max(30).nullable().optional(), - wecomUserId: z.string().trim().max(128).nullable().optional(), - enabled: z.boolean().optional(), -}); - -export const wecomContactQuerySchema = z.object({ - keyword: z.string().trim().optional(), - status: z.string().trim().optional(), - take: z.coerce.number().int().min(1).max(200).default(100), - skip: z.coerce.number().int().min(0).default(0), -}); - -export const wecomContactLinkSchema = z.object({ - miniProgramUserId: z.string().uuid().nullable().optional(), - leadId: z.string().uuid().nullable().optional(), -}); diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index e3e59a1..c5d9d5e 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -8,7 +8,6 @@ import { prisma } from "./prisma.js"; import { registerAdminRoutes } from "./routes/admin.js"; import { registerMemoryRoutes } from "./routes/memory.js"; import { registerPublicRoutes } from "./routes/public.js"; -import { registerWecomRoutes } from "./routes/wecom.js"; export async function createServer() { const app = Fastify({ @@ -22,10 +21,6 @@ export async function createServer() { credentials: true, }); - app.addContentTypeParser(["application/xml", "text/xml"], { parseAs: "string" }, (_request, body, done) => { - done(null, body); - }); - await app.register(jwt, { secret: process.env.JWT_SECRET ?? "dev-only-change-me-before-production", }); @@ -73,7 +68,6 @@ export async function createServer() { }); await registerPublicRoutes(app); await registerAdminRoutes(app); - await registerWecomRoutes(app); } return app; diff --git a/apps/api/src/wecom.ts b/apps/api/src/wecom.ts deleted file mode 100644 index 7eb8118..0000000 --- a/apps/api/src/wecom.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; - -export const DEFAULT_WECOM_CONTACT_WAY_CONFIG_ID = "5f8a2d2756e2c69776c186ef28fc48ee"; -export const WECOM_CONTACT_PLUGIN_ID = "wx104a1a20c3f81ec2"; -export const WECOM_CONTACT_PLUGIN_VERSION = "1.4.3"; - -export type WecomContactWayConfig = { - configId: string; - type: number; - scene: number; - style: number; - remark: string | null; - state: string | null; - wecomUserId: string | null; - enabled: boolean; -}; - -type WecomApiResponse = { - errcode?: number; - errmsg?: string; - [key: string]: unknown; -}; - -type WecomAccessTokenResponse = WecomApiResponse & { access_token?: string; expires_in?: number }; - -export type WecomCallbackEvent = { - infoType: string | null; - changeType: string | null; - userId: string | null; - externalUserId: string | null; - state: string | null; - authCorpId: string | null; - eventTime: Date; - raw: Record; -}; - -let accessTokenCache: { token: string; expiresAt: number } | null = null; - -function env(name: string) { - return process.env[name]?.trim() || ""; -} - -export function getWecomContactWayConfigDefaults(): WecomContactWayConfig { - return { - configId: env("WECOM_CONTACT_WAY_CONFIG_ID") || DEFAULT_WECOM_CONTACT_WAY_CONFIG_ID, - type: Number(env("WECOM_CONTACT_WAY_TYPE") || 1), - scene: Number(env("WECOM_CONTACT_WAY_SCENE") || 1), - style: Number(env("WECOM_CONTACT_WAY_STYLE") || 1), - remark: env("WECOM_CONTACT_WAY_REMARK") || null, - state: env("WECOM_CONTACT_WAY_STATE") || null, - wecomUserId: env("WECOM_CONTACT_WAY_USER_ID") || null, - enabled: env("WECOM_CONTACT_WAY_ENABLED") !== "0", - }; -} - -export function hasWecomApiCredentials() { - return Boolean(env("WECOM_CORP_ID") && env("WECOM_CORP_SECRET")); -} - -export function hasWecomCallbackCredentials() { - return Boolean(env("WECOM_CALLBACK_TOKEN") && env("WECOM_CALLBACK_AES_KEY")); -} - -function apiUrl(path: string, params: Record) { - const url = new URL(`https://qyapi.weixin.qq.com${path}`); - Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value)); - return url; -} - -async function readWecomJson(url: URL, init?: RequestInit) { - const response = await fetch(url, init); - const body = (await response.json()) as T; - if (!response.ok) throw new Error(`企微接口请求失败:HTTP ${response.status}`); - if (typeof body.errcode === "number" && body.errcode !== 0) { - throw new Error(`企微接口失败:${body.errmsg ?? body.errcode}`); - } - return body; -} - -export async function getWecomAccessToken(forceRefresh = false) { - if (!hasWecomApiCredentials()) throw new Error("企微服务端尚未配置 WECOM_CORP_ID / WECOM_CORP_SECRET"); - if (!forceRefresh && accessTokenCache && accessTokenCache.expiresAt > Date.now() + 60_000) return accessTokenCache.token; - - const body = await readWecomJson(apiUrl("/cgi-bin/gettoken", { - corpid: env("WECOM_CORP_ID"), - corpsecret: env("WECOM_CORP_SECRET"), - })); - if (!body.access_token) throw new Error("企微接口未返回 access_token"); - accessTokenCache = { - token: body.access_token, - expiresAt: Date.now() + Math.max(60, Number(body.expires_in ?? 7200) - 120) * 1000, - }; - return body.access_token; -} - -export async function getWecomContactWay(configId = getWecomContactWayConfigDefaults().configId) { - const accessToken = await getWecomAccessToken(); - return readWecomJson(apiUrl("/cgi-bin/externalcontact/get_contact_way", { access_token: accessToken }), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ config_id: configId }), - }); -} - -export async function getWecomExternalContact(externalUserId: string, userId: string) { - const accessToken = await getWecomAccessToken(); - return readWecomJson; - }; - follow_info?: { - userid?: string; - remark?: string; - description?: string; - createtime?: number; - state?: string; - remark_corp_name?: string; - }; - }>(apiUrl("/cgi-bin/externalcontact/get", { - access_token: accessToken, - external_userid: externalUserId, - userid: userId, - })); -} - -export function sha1Signature(values: string[]) { - return createHash("sha1").update(values.sort().join(""), "utf8").digest("hex"); -} - -function decodeCallbackKey() { - const key = env("WECOM_CALLBACK_AES_KEY"); - if (!key) throw new Error("企微回调尚未配置 WECOM_CALLBACK_AES_KEY"); - return Buffer.from(`${key}=`, "base64"); -} - -function decryptCallbackPayload(encrypted: string) { - const key = decodeCallbackKey(); - if (key.length !== 32) throw new Error("WECOM_CALLBACK_AES_KEY 长度不正确"); - const encryptedBuffer = Buffer.from(encrypted, "base64"); - const decipher = createDecipheriv("aes-256-cbc", key, key.subarray(0, 16)); - decipher.setAutoPadding(false); - const decrypted = Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]); - const pad = decrypted[decrypted.length - 1] ?? 0; - if (pad < 1 || pad > 32) throw new Error("企微回调解密填充不正确"); - const unpadded = decrypted.subarray(0, decrypted.length - pad); - if (unpadded.length < 20) throw new Error("企微回调解密内容不完整"); - const messageLength = unpadded.readUInt32BE(16); - const messageStart = 20; - return unpadded.subarray(messageStart, messageStart + messageLength).toString("utf8"); -} - -function encryptCallbackPayload(message: string) { - const key = decodeCallbackKey(); - if (key.length !== 32) throw new Error("WECOM_CALLBACK_AES_KEY 长度不正确"); - const random = randomBytes(16); - const messageBuffer = Buffer.from(message, "utf8"); - const corpId = Buffer.from(env("WECOM_CORP_ID"), "utf8"); - const unpadded = Buffer.concat([random, Buffer.alloc(4), messageBuffer, corpId]); - unpadded.writeUInt32BE(messageBuffer.length, 16); - const blockSize = 32; - const remainder = unpadded.length % blockSize; - const padLength = remainder === 0 ? blockSize : blockSize - remainder; - const padding = Buffer.alloc(padLength, padLength); - const cipher = createCipheriv("aes-256-cbc", key, key.subarray(0, 16)); - return Buffer.concat([cipher.update(Buffer.concat([unpadded, padding])), cipher.final()]).toString("base64"); -} - -function xmlTag(xml: string, tag: string) { - const expression = new RegExp(`<${tag}(?:>|>([\\s\\S]*?))`); - const match = xml.match(expression); - return (match?.[1] ?? match?.[2] ?? "").trim(); -} - -export function parseWecomXml(xml: string) { - const tags = [ - "ToUserName", "FromUserName", "CreateTime", "MsgType", "Event", "EventKey", "ChangeType", - "InfoType", "UserID", "ExternalUserID", "State", "WelcomeCode", "AuthCorpId", "Encrypt", - ]; - return Object.fromEntries(tags.map((tag) => [tag, xmlTag(xml, tag)]).filter(([, value]) => value)) as Record; -} - -export function verifyWecomCallback(signature: string, timestamp: string, nonce: string, encrypted: string) { - const token = env("WECOM_CALLBACK_TOKEN"); - if (!token || !signature || !timestamp || !nonce || !encrypted) return false; - return sha1Signature([token, timestamp, nonce, encrypted]) === signature; -} - -export function decryptWecomCallback(encrypted: string) { - return decryptCallbackPayload(encrypted); -} - -export function encryptWecomCallback(message: string) { - return encryptCallbackPayload(message); -} - -export function normalizeWecomCallbackEvent(xml: string): WecomCallbackEvent { - const raw = parseWecomXml(xml); - const timestamp = Number(raw.CreateTime ?? 0); - return { - infoType: raw.InfoType || null, - changeType: raw.ChangeType || raw.Event || null, - userId: raw.UserID || null, - externalUserId: raw.ExternalUserID || null, - state: raw.State || raw.EventKey || null, - authCorpId: raw.AuthCorpId || raw.ToUserName || null, - eventTime: Number.isFinite(timestamp) && timestamp > 0 ? new Date(timestamp * 1000) : new Date(), - raw, - }; -} diff --git a/apps/miniprogram/README.md b/apps/miniprogram/README.md index 5e775dc..66244eb 100644 --- a/apps/miniprogram/README.md +++ b/apps/miniprogram/README.md @@ -2,7 +2,7 @@ 这是唯一的用户端前台,基于 Taro + React + TypeScript,覆盖探索、搜索、线路详情、收藏、最近浏览、微信登录、手机号授权、我的需求和原生企微“联系我”页。 -小程序已声明企微官方插件 `wx104a1a20c3f81ec2`(版本 `1.4.3`)。`pages/contact/index` 通过公开 API 读取当前联系我 `config_id`,再渲染 `plugin://contactPlugin/cell`;H5 中的“企微联系”入口只负责跳转到该原生页。 +小程序已声明企微官方插件 `wx104a1a20c3f81ec2`(版本 `1.4.3`)。`pages/contact/index` 内置企业微信后台创建的联系我 `config_id`,直接渲染 `plugin://contactPlugin/cell`;H5 中的“企微联系”入口只负责跳转到该原生页。后端不参与联系我插件的配置、同步或回调。 ## 本地开发 @@ -30,6 +30,6 @@ TARO_APP_API_BASE_URL="https://biz.wanderqtrip.com/api" npm run build:miniapp ``` -生产 API 需要配置 `WECHAT_APP_ID`、`WECHAT_APP_SECRET`、`DATABASE_URL` 和 `JWT_SECRET`,企微对接还需要按 `docs/backend/README.md` 配置 `WECOM_*` 服务端变量,并将 API 域名与 OSS/CDN 域名加入微信小程序的 `request`、`downloadFile` 合法域名。图片统一使用 API 返回的 OSS WebP 公共 URL,不打包本地图片素材。 +生产 API 需要配置 `WECHAT_APP_ID`、`WECHAT_APP_SECRET`、`DATABASE_URL` 和 `JWT_SECRET`,并将 API 域名与 OSS/CDN 域名加入微信小程序的 `request`、`downloadFile` 合法域名。联系我插件的 `config_id` 已随小程序代码发布,不需要后端企微密钥。图片统一使用 API 返回的 OSS WebP 公共 URL,不打包本地图片素材。 数据库首次部署执行 `npm run db:migrate`,初始化内容执行 `npm run db:seed`。 diff --git a/apps/miniprogram/src/pages/contact/index.tsx b/apps/miniprogram/src/pages/contact/index.tsx index acc5fde..d953317 100644 --- a/apps/miniprogram/src/pages/contact/index.tsx +++ b/apps/miniprogram/src/pages/contact/index.tsx @@ -1,72 +1,26 @@ import { Button, Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; -import { useEffect, useState } from "react"; - -import { apiUrl } from "../../services/api"; const SUPPORT_PHONE = "18786174929"; - -type ContactWayResponse = { - enabled: boolean; - configId: string; - type: number; - scene: number; - style: number; - remark?: string | null; - state?: string | null; - pluginId: string; - pluginVersion: string; -}; - -const DEFAULT_CONTACT_WAY: ContactWayResponse = { - enabled: true, - configId: "5f8a2d2756e2c69776c186ef28fc48ee", - type: 1, - scene: 1, - style: 1, - remark: null, - state: null, - pluginId: "wx104a1a20c3f81ec2", - pluginVersion: "1.4.3", -}; +const CONTACT_WAY_CONFIG_ID = "5f8a2d2756e2c69776c186ef28fc48ee"; +const CONTACT_WAY_STYLE = 1; export default function ContactPage() { - const [contactWay, setContactWay] = useState(DEFAULT_CONTACT_WAY); - - useEffect(() => { - let active = true; - Taro.request({ url: apiUrl("/api/public/wecom/contact-way") }) - .then((response) => { - if (!active) return; - if (response.statusCode >= 200 && response.statusCode < 300 && response.data?.configId) { - setContactWay(response.data); - } - }) - .catch(() => { - if (active) console.warn("企微联系配置接口暂时不可达,使用内置联系我配置", DEFAULT_CONTACT_WAY.configId); - }); - return () => { - active = false; - }; - }, []); - const callSupport = () => { void Taro.makePhoneCall({ phoneNumber: SUPPORT_PHONE }); }; - const showPlugin = Boolean(contactWay.enabled && contactWay.configId); - return ( 联系万趣定制管家 联系万趣定制管家 - {showPlugin ? ( + {CONTACT_WAY_CONFIG_ID ? ( 添加企微服务管家 点击按钮后将进入企微添加流程 - + ) : (