diff --git a/apps/admin-web/src/lib/constants.ts b/apps/admin-web/src/lib/constants.ts index bfb5a3c..48ec1ca 100644 --- a/apps/admin-web/src/lib/constants.ts +++ b/apps/admin-web/src/lib/constants.ts @@ -128,3 +128,18 @@ export const LEDGER_TYPE_LABELS: Record = { export function fmtTime(v?: string | null) { return v ? new Date(v).toLocaleString('zh-CN') : '—'; } + +/** 手机号脱敏展示:138****5678;空值返回 — */ +export function maskPhone(phone?: string | null): string { + const raw = String(phone ?? '').trim(); + if (!raw) return '—'; + const digits = raw.replace(/\D/g, ''); + if (digits.length >= 11) { + return `${digits.slice(0, 3)}****${digits.slice(-4)}`; + } + if (digits.length >= 7) { + return `${digits.slice(0, 3)}****${digits.slice(-2)}`; + } + if (digits.length > 0) return `${digits.slice(0, 1)}****`; + return '****'; +} diff --git a/apps/admin-web/src/pages/UsersPage.tsx b/apps/admin-web/src/pages/UsersPage.tsx index c3f1f32..90b2072 100644 --- a/apps/admin-web/src/pages/UsersPage.tsx +++ b/apps/admin-web/src/pages/UsersPage.tsx @@ -19,7 +19,7 @@ import { import type { ColumnsType } from 'antd/es/table'; import { USER_SOURCE_TYPE_LABELS, type UserSourceType } from '@dukang/shared-types'; import { request, type AdminUserRow, type Paginated } from '../lib/api'; -import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants'; +import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants'; type UserOrderRow = { id: string; @@ -222,7 +222,7 @@ export default function UsersPage() { title: '手机', dataIndex: 'phone', width: 120, - render: (v) => v || '—', + render: (v) => maskPhone(v), }, { title: '验手机', @@ -385,7 +385,7 @@ export default function UsersPage() { {detail.id} {detail.userNo} {detail.nickname || '—'} - {detail.phone || '—'} + {maskPhone(detail.phone)} {detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'} @@ -419,7 +419,7 @@ export default function UsersPage() { {detail.deviceKey || '—'} {detail.mergedInto - ? `${detail.mergedInto.userNo} (${detail.mergedInto.phone || '无手机'})` + ? `${detail.mergedInto.userNo} (${detail.mergedInto.phone ? maskPhone(detail.mergedInto.phone) : '无手机'})` : '—'} {detail.mergedFromCount ?? 0} @@ -593,7 +593,7 @@ export default function UsersPage() { columns={[ { title: '用户编号', dataIndex: 'userNo', width: 120 }, { title: '昵称', dataIndex: 'nickname', width: 100, render: (v) => v || '—' }, - { title: '手机', dataIndex: 'phone', width: 120, render: (v) => v || '—' }, + { title: '手机', dataIndex: 'phone', width: 120, render: (v) => maskPhone(v) }, { title: '风险', width: 200, diff --git a/apps/admin-web/src/pages/WechatBindingsPage.tsx b/apps/admin-web/src/pages/WechatBindingsPage.tsx index b823e94..8ef2944 100644 --- a/apps/admin-web/src/pages/WechatBindingsPage.tsx +++ b/apps/admin-web/src/pages/WechatBindingsPage.tsx @@ -5,7 +5,7 @@ import { import type { ColumnsType } from 'antd/es/table'; import { request } from '../lib/api'; import { AdminCellLine } from '../components/AdminCellLine'; -import { fmtTime } from '../lib/constants'; +import { fmtTime, maskPhone } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; type ActorType = 'USER' | 'STORE' | 'PARTNER' | 'HQ'; @@ -119,7 +119,7 @@ export default function WechatBindingsPage() { title: '手机号', dataIndex: 'primaryPhone', width: 140, - render: (v) => v || '—', + render: (v) => maskPhone(v), }, { title: '身份摘要', @@ -128,7 +128,7 @@ export default function WechatBindingsPage() { ACTOR_TYPE_LABELS[i.actorType]).join(' / ')} secondary={r.identities - .map((i) => i.refLabel || i.name || i.phone) + .map((i) => i.refLabel || i.name || (i.phone ? maskPhone(i.phone) : '')) .filter(Boolean) .join(' · ')} /> @@ -164,7 +164,7 @@ export default function WechatBindingsPage() { render: (_, r) => ( ), }, diff --git a/apps/mini-user/src/pages/benefit/index.config.ts b/apps/mini-user/src/pages/benefit/index.config.ts index 204c17e..afb248a 100644 --- a/apps/mini-user/src/pages/benefit/index.config.ts +++ b/apps/mini-user/src/pages/benefit/index.config.ts @@ -3,4 +3,6 @@ export default definePageConfig({ navigationBarTitleText: '好客权益', enablePullDownRefresh: true, backgroundTextStyle: 'dark', + enableShareAppMessage: true, + enableShareTimeline: true, }); diff --git a/apps/mini-user/src/pages/benefit/index.tsx b/apps/mini-user/src/pages/benefit/index.tsx index 9a71070..bbe957f 100644 --- a/apps/mini-user/src/pages/benefit/index.tsx +++ b/apps/mini-user/src/pages/benefit/index.tsx @@ -1,11 +1,18 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { View, Text } from '@tarojs/components'; -import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; +import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; +import ShareNavButton from '../../components/ShareNavButton'; +import WechatShareReady from '../../components/WechatShareReady'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; import { goLogin } from '../../lib/auth-nav'; import { isLoggedIn, request, toast } from '../../lib/api'; import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar'; +import { + DEFAULT_SHARE_DESC, + DEFAULT_SHARE_TITLE, + toWeappShareMessage, +} from '../../lib/wechat-share'; type BenefitSummary = { totalBalance: number; @@ -84,8 +91,25 @@ export default function BenefitPage() { const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID'); const visible = tab === 'available' ? available : history; + const sharePayload = useMemo( + () => ({ + title: '好客权益 · 杜康好客', + desc: DEFAULT_SHARE_DESC, + path: '/pages/benefit/index', + }), + [], + ); + + useShareAppMessage(() => toWeappShareMessage(sharePayload)); + useShareTimeline(() => ({ + title: sharePayload.title || DEFAULT_SHARE_TITLE, + query: '', + imageUrl: sharePayload.imgUrl, + })); + return ( + {process.env.TARO_ENV !== 'h5' ? ( 好客权益 @@ -98,6 +122,9 @@ export default function BenefitPage() { 郑州市 + + + diff --git a/apps/mini-user/src/pages/home/index.config.ts b/apps/mini-user/src/pages/home/index.config.ts index f25a67f..4edb423 100644 --- a/apps/mini-user/src/pages/home/index.config.ts +++ b/apps/mini-user/src/pages/home/index.config.ts @@ -2,4 +2,6 @@ export default definePageConfig({ navigationBarTitleText: '杜康好客', enablePullDownRefresh: true, backgroundTextStyle: 'dark', + enableShareAppMessage: true, + enableShareTimeline: true, }); diff --git a/apps/mini-user/src/pages/home/index.tsx b/apps/mini-user/src/pages/home/index.tsx index c632f42..eee7846 100644 --- a/apps/mini-user/src/pages/home/index.tsx +++ b/apps/mini-user/src/pages/home/index.tsx @@ -1,9 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components'; -import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; +import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro'; import PageShell from '../../components/PageShell'; import TabMainHeader from '../../components/TabMainHeader'; import CouponBadge from '../../components/CouponBadge'; +import WechatShareReady from '../../components/WechatShareReady'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; import { goLogin } from '../../lib/auth-nav'; import { isLoggedIn, request, toast } from '../../lib/api'; @@ -11,6 +12,11 @@ import { ensurePayReady } from '../../lib/pay-ready'; import { capturePromoSceneAndTouchScan } from '../../lib/promo'; import { getProductMainImage } from '../../lib/product-images'; import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location'; +import { + DEFAULT_SHARE_DESC, + DEFAULT_SHARE_TITLE, + toWeappShareMessage, +} from '../../lib/wechat-share'; type Product = { id: string; @@ -139,8 +145,26 @@ export default function HomePage() { const banners = miniHome.banners; const footerUrl = miniHome.footerUrl; + const sharePayload = useMemo( + () => ({ + title: DEFAULT_SHARE_TITLE, + desc: DEFAULT_SHARE_DESC, + path: '/pages/home/index', + imgUrl: banners[0] || undefined, + }), + [banners], + ); + + useShareAppMessage(() => toWeappShareMessage(sharePayload)); + useShareTimeline(() => ({ + title: sharePayload.title || DEFAULT_SHARE_TITLE, + query: '', + imageUrl: sharePayload.imgUrl, + })); + return ( + {banners.length > 0 ? ( diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx index 64ba1d0..5cc8bb1 100644 --- a/apps/mini-user/src/pages/mine/index.tsx +++ b/apps/mini-user/src/pages/mine/index.tsx @@ -1,11 +1,15 @@ import { useEffect, useState } from 'react'; import { View, Text, Image, Button, Input, ScrollView } from '@tarojs/components'; import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro'; -import { isWxAuthorizeEnabled, type ClientRuntimeConfig } from '@dukang/shared-types'; +import { + BRAND_LOGO_MARK_URL, + QUALIFICATION_DISCLOSURE_URL, + isWxAuthorizeEnabled, + type ClientRuntimeConfig, +} from '@dukang/shared-types'; import PageShell from '../../components/PageShell'; import TabMainHeader from '../../components/TabMainHeader'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; -import { BRAND_LOGO_MARK_URL } from '@dukang/shared-types'; import { goLogin } from '../../lib/auth-nav'; import { bindWechatForUser } from '../../lib/wechat-auth'; import { @@ -18,7 +22,6 @@ import { } from '../../lib/mini-wechat-profile'; import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api'; import { isWechatEnv } from '../../lib/weixin'; -import qualificationDisclosureImg from '../../assets/qualification-disclosure.png'; const ORDER_SHORTCUTS = [ { tab: 'pending_pay', icon: '付', label: '待付款' }, @@ -533,7 +536,7 @@ export default function MinePage() { diff --git a/apps/mini-user/src/styles/benefit.css b/apps/mini-user/src/styles/benefit.css index 9f79285..72605ae 100644 --- a/apps/mini-user/src/styles/benefit.css +++ b/apps/mini-user/src/styles/benefit.css @@ -50,6 +50,14 @@ z-index: 2; } +.benefit-header__share .page-nav-bar__btn { + background: transparent; +} + +.benefit-header__share .page-nav-bar__icon { + color: var(--color-heritage-red); +} + .benefit-header-city-pin { width: 8px; height: 8px; diff --git a/packages/shared-types/src/config.ts b/packages/shared-types/src/config.ts index 6ea9f73..150bd73 100644 --- a/packages/shared-types/src/config.ts +++ b/packages/shared-types/src/config.ts @@ -42,6 +42,13 @@ export const BRAND_LOGO_WIDE_URL = `${BRAND_LOGO_OSS_BASE}logo1.png`; /** 仅图标 Logo(默认头像:未微信授权时) */ export const BRAND_LOGO_MARK_URL = `${BRAND_LOGO_OSS_BASE}logo2.png`; +/** 小程序静态资源(资质公示等) */ +export const MINI_USER_STATIC_OSS_BASE = + 'https://dukang-dev.oss-cn-beijing.aliyuncs.com/static/mini-user/'; + +/** 「我的」页资质公示长图 */ +export const QUALIFICATION_DISCLOSURE_URL = `${MINI_USER_STATIC_OSS_BASE}qualification-disclosure.png`; + /** 总部客服电话(C 端联系客服) */ export const CUSTOMER_SERVICE_PHONE = '400-888-1234'; diff --git a/server/dukang-api/scripts/upload-qualification-disclosure.mjs b/server/dukang-api/scripts/upload-qualification-disclosure.mjs new file mode 100644 index 0000000..6611a13 --- /dev/null +++ b/server/dukang-api/scripts/upload-qualification-disclosure.mjs @@ -0,0 +1,83 @@ +/** + * 上传小程序「资质公示」静态图到 OSS:static/mini-user/qualification-disclosure.png + * 用法(在 server/dukang-api): node scripts/upload-qualification-disclosure.mjs + */ +import { readFileSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const OSS = require('ali-oss'); + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const apiRoot = resolve(__dirname, '..'); +const repoRoot = resolve(apiRoot, '../..'); + +function loadEnvFile(path) { + if (!existsSync(path)) return {}; + const out = {}; + for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) { + const m = line.match(/^([A-Z0-9_]+)=(.*)$/); + if (!m) continue; + let v = m[2]; + if ( + (v.startsWith('"') && v.endsWith('"')) || + (v.startsWith("'") && v.endsWith("'")) + ) { + v = v.slice(1, -1); + } + out[m[1]] = v; + } + return out; +} + +const env = { + ...loadEnvFile(resolve(apiRoot, '.env.production')), + ...loadEnvFile(resolve(apiRoot, '.env.development')), + ...loadEnvFile(resolve(apiRoot, '.env')), +}; + +const accessKeyId = env.OSS_ACCESS_KEY_ID || ''; +const accessKeySecret = env.OSS_ACCESS_KEY_SECRET || ''; +const bucket = env.OSS_BUCKET || ''; +const region = env.OSS_REGION || 'oss-cn-beijing'; +const cdnBase = (env.OSS_CDN_BASE || '').replace(/\/$/, ''); + +if (!accessKeyId || !accessKeySecret || !bucket) { + console.error('缺少 OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET / OSS_BUCKET'); + process.exit(1); +} + +const localFile = resolve( + repoRoot, + 'apps/mini-user/src/assets/qualification-disclosure.png', +); +if (!existsSync(localFile)) { + console.error('本地文件不存在:', localFile); + process.exit(1); +} + +const ossKey = 'static/mini-user/qualification-disclosure.png'; +const client = new OSS({ + region, + accessKeyId, + accessKeySecret, + bucket, +}); + +const buffer = readFileSync(localFile); +await client.put(ossKey, buffer, { + mime: 'image/png', + headers: { + 'Content-Disposition': 'inline', + 'Cache-Control': 'public, max-age=31536000', + }, +}); + +const url = cdnBase + ? `${cdnBase}/${ossKey}` + : `https://${bucket}.${region}.aliyuncs.com/${ossKey}`; + +console.log('uploaded:', ossKey); +console.log('url:', url);