From 0e711be6c6b058c763f26230d987298d1091b0ff Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Thu, 3 Sep 2026 23:28:37 +0800 Subject: [PATCH] =?UTF-8?q?v4.0.17=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/h5-partner/src/pages/HomePage.tsx | 8 ++- apps/h5-partner/src/pages/LeaderboardPage.tsx | 8 ++- .../src/components/StoreRedeemMarquee.tsx | 14 ++-- apps/mini-user/src/lib/datetime.ts | 37 +++++++++++ apps/mini-user/src/pages/home/index.tsx | 6 +- apps/mini-user/src/pages/login/index.tsx | 53 +++++++-------- .../src/pages/order-confirm-pickup/index.tsx | 4 -- .../src/pages/order-confirm/index.tsx | 4 -- .../src/pages/product-detail/index.tsx | 5 -- .../src/pages/redeem-success/index.tsx | 49 ++++++++++---- .../src/pages/store-detail/index.tsx | 26 +++++++- .../store-package-detail/index.config.ts | 4 +- .../src/pages/store-package-detail/index.tsx | 65 +++++++++++++++---- apps/mini-user/src/pages/stores/index.tsx | 4 +- .../src/modules/redeem/redeem.controller.ts | 5 ++ .../src/modules/redeem/redeem.module.ts | 9 ++- .../src/modules/redeem/redeem.service.ts | 22 +++++++ .../src/modules/store/store.service.ts | 15 +++++ 18 files changed, 254 insertions(+), 84 deletions(-) diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx index 6c4bf2b..cb34681 100644 --- a/apps/h5-partner/src/pages/HomePage.tsx +++ b/apps/h5-partner/src/pages/HomePage.tsx @@ -128,8 +128,12 @@ function LeaderboardPreview({ entries }: { entries: PartnerLeaderboardEntry[] })

累计拓店: {entry.totalStores} 间

-

{entry.periodStores} 间

-

本月新增

+ {entry.periodStores > 0 ? ( + <> +

{entry.periodStores} 间

+

本月新增

+ + ) : null}
)) diff --git a/apps/h5-partner/src/pages/LeaderboardPage.tsx b/apps/h5-partner/src/pages/LeaderboardPage.tsx index 0136c75..330fe09 100644 --- a/apps/h5-partner/src/pages/LeaderboardPage.tsx +++ b/apps/h5-partner/src/pages/LeaderboardPage.tsx @@ -117,8 +117,12 @@ export default function LeaderboardPage() {

累计拓店 {entry.totalStores} 间

-

{entry.periodStores} 间

-

{periodSubLabel(period)}

+ {entry.periodStores > 0 ? ( + <> +

{entry.periodStores} 间

+

{periodSubLabel(period)}

+ + ) : null}
)) diff --git a/apps/mini-user/src/components/StoreRedeemMarquee.tsx b/apps/mini-user/src/components/StoreRedeemMarquee.tsx index 5b7c618..8b4311d 100644 --- a/apps/mini-user/src/components/StoreRedeemMarquee.tsx +++ b/apps/mini-user/src/components/StoreRedeemMarquee.tsx @@ -1,28 +1,33 @@ import { useMemo } from 'react'; import { View, Text, Swiper, SwiperItem } from '@tarojs/components'; +import { formatRedeemRelativeTime } from '../lib/datetime'; export type StoreRedeemMarqueeItem = { userLabel: string; amount: string; + createdAt?: string | null; }; type StoreRedeemMarqueeProps = { items: StoreRedeemMarqueeItem[]; }; -const STAY_MS = 20000; +const STAY_MS = 5000; function MarqueeRow({ item }: { item: StoreRedeemMarqueeItem }) { + const timeLabel = formatRedeemRelativeTime(item.createdAt); return ( - {item.userLabel} 刚刚到店核销 + + {item.userLabel} {timeLabel}到店核销 + {item.amount} ); } -/** 核销记录:单条静止;多条竖向循环,每条停留 20 秒 */ +/** 核销记录:单条静止;多条竖向循环,支持手动滑动,每条停留 5 秒 */ export default function StoreRedeemMarquee({ items }: StoreRedeemMarqueeProps) { const list = useMemo( () => @@ -30,6 +35,7 @@ export default function StoreRedeemMarquee({ items }: StoreRedeemMarqueeProps) { .map((row) => ({ userLabel: String(row.userLabel || '用户***').trim() || '用户***', amount: String(row.amount || '').trim(), + createdAt: row.createdAt ?? null, })) .filter((row) => row.amount), [items], @@ -57,7 +63,7 @@ export default function StoreRedeemMarquee({ items }: StoreRedeemMarqueeProps) { indicatorDots={false} > {list.map((row, index) => ( - + ))} diff --git a/apps/mini-user/src/lib/datetime.ts b/apps/mini-user/src/lib/datetime.ts index dbc16b7..ea3d5d2 100644 --- a/apps/mini-user/src/lib/datetime.ts +++ b/apps/mini-user/src/lib/datetime.ts @@ -1,3 +1,40 @@ +const MS_MINUTE = 60_000; +const MS_HOUR = 60 * MS_MINUTE; +const MS_DAY = 24 * MS_HOUR; + +function shanghaiParts(input: string | Date) { + const d = input instanceof Date ? input : new Date(input); + if (Number.isNaN(d.getTime())) return null; + const sh = new Date(d.getTime() + 8 * 60 * 60 * 1000); + const p = (n: number) => String(n).padStart(2, '0'); + return { + year: sh.getUTCFullYear(), + month: p(sh.getUTCMonth() + 1), + day: p(sh.getUTCDate()), + hour: p(sh.getUTCHours()), + minute: p(sh.getUTCMinutes()), + }; +} + +/** 核销走马灯时间:24 小时内相对时间,超过则显示具体时间 */ +export function formatRedeemRelativeTime(input?: string | Date | null): string { + if (input == null || input === '') return '刚刚'; + const at = input instanceof Date ? input : new Date(input); + if (Number.isNaN(at.getTime())) return '刚刚'; + const diffMs = Date.now() - at.getTime(); + if (diffMs < MS_MINUTE) return '刚刚'; + if (diffMs < MS_HOUR) return `${Math.floor(diffMs / MS_MINUTE)}分钟前`; + if (diffMs < MS_DAY) return `${Math.floor(diffMs / MS_HOUR)}小时前`; + const parts = shanghaiParts(at); + if (!parts) return '刚刚'; + const nowParts = shanghaiParts(new Date()); + const datePrefix = + nowParts && parts.year === nowParts.year + ? `${parts.month}-${parts.day}` + : `${parts.year}-${parts.month}-${parts.day}`; + return `${datePrefix} ${parts.hour}:${parts.minute}`; +} + /** 格式化为 Asia/Shanghai:2026-08-03 15:14:30(不依赖 Intl,兼容微信小程序) */ export function formatShanghaiDateTime(input?: string | Date | null): string { if (input == null || input === '') return '—'; diff --git a/apps/mini-user/src/pages/home/index.tsx b/apps/mini-user/src/pages/home/index.tsx index ddcd9fb..169fa91 100644 --- a/apps/mini-user/src/pages/home/index.tsx +++ b/apps/mini-user/src/pages/home/index.tsx @@ -22,7 +22,7 @@ import { isHomeCatalogBootstrapped, setHomeCatalogCache, } from '../../lib/home-catalog-session'; -import { ensurePayReady } from '../../lib/pay-ready'; + import { capturePromoSceneAndTouchScan } from '../../lib/promo'; import { getProductMainImage } from '../../lib/product-images'; import { @@ -202,8 +202,6 @@ export default function HomePage() { goLogin(returnPath); return; } - const ready = await ensurePayReady(returnPath); - if (!ready) return; Taro.navigateTo({ url: returnPath }); } @@ -213,8 +211,6 @@ export default function HomePage() { goLogin(returnPath); return; } - const ready = await ensurePayReady(returnPath); - if (!ready) return; Taro.navigateTo({ url: returnPath }); } diff --git a/apps/mini-user/src/pages/login/index.tsx b/apps/mini-user/src/pages/login/index.tsx index 7279c9e..70e783d 100644 --- a/apps/mini-user/src/pages/login/index.tsx +++ b/apps/mini-user/src/pages/login/index.tsx @@ -102,7 +102,7 @@ export default function LoginPage() { const [wxSessionKey, setWxSessionKey] = useState(initialWxSessionKey); const [wxAuthorize, setWxAuthorize] = useState(true); const [completeMode, setCompleteMode] = useState<'phone' | 'wechat' | null>(null); - const [showSmsForm, setShowSmsForm] = useState(!IS_WEAPP); + const [showSmsForm, setShowSmsForm] = useState(true); const [logoWideUrl, setLogoWideUrl] = useState(() => getBrandAssetsSync().brandLogoWideUrl); useEffect(() => { @@ -393,7 +393,7 @@ export default function LoginPage() { ? '绑定手机号' : completeMode === 'wechat' ? '授权登录' - : '手机号快捷登录'; + : '验证码登录'; return ( @@ -451,32 +451,9 @@ export default function LoginPage() { setAgreed((v) => !v)} /> - {showPhoneQuick ? ( - ensureAgreed()} - onGetPhoneNumber={(phoneCode) => void onPhoneQuickLogin(phoneCode)} - onFail={(message) => setMsg(message)} - /> - ) : null} - - {showPhoneQuick ? ( - - - setShowSmsForm((v) => !v)} - > - {showSmsForm ? '收起验证码登录' : '使用验证码登录'} - - - - ) : null} - {(showSmsForm || !showPhoneQuick) && ( <> - + +86 )} + {showPhoneQuick ? ( + <> + + + 或者 + + + ensureAgreed()} + onGetPhoneNumber={(phoneCode) => void onPhoneQuickLogin(phoneCode)} + onFail={(message) => setMsg(message)} + /> + setShowSmsForm((v) => !v)} + > + {showSmsForm ? '收起验证码登录' : '使用验证码登录'} + + + ) : null} + {displayMsg ? ( {displayMsg} diff --git a/apps/mini-user/src/pages/order-confirm-pickup/index.tsx b/apps/mini-user/src/pages/order-confirm-pickup/index.tsx index 1f1427c..bcad216 100644 --- a/apps/mini-user/src/pages/order-confirm-pickup/index.tsx +++ b/apps/mini-user/src/pages/order-confirm-pickup/index.tsx @@ -6,7 +6,6 @@ import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { goLogin } from '../../lib/auth-nav'; import { buildPayUrl } from '../../lib/checkout-nav'; -import { ensurePayReady } from '../../lib/pay-ready'; import { fetchUserProfile } from '../../lib/pay-wechat'; import { request, toast } from '../../lib/api'; import { getProductMainImage } from '../../lib/product-images'; @@ -150,9 +149,6 @@ export default function OrderConfirmPickupPage() { } } - const ready = await ensurePayReady(returnPath); - if (!ready) return; - const { confirm } = await Taro.showModal({ title: '确认提交订单', content: `请确保您已拿到货品,货款将直接打给商家,如不是现场交易请选择立即购买方式下单,我们会为您安排配送到家。`, diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx index 747e3f0..2281c09 100644 --- a/apps/mini-user/src/pages/order-confirm/index.tsx +++ b/apps/mini-user/src/pages/order-confirm/index.tsx @@ -8,7 +8,6 @@ import { goLogin } from '../../lib/auth-nav'; import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav'; import { tryGetClientGpsLocation } from '../../lib/client-location'; import { maskPhone } from '../../lib/phone'; -import { ensurePayReady } from '../../lib/pay-ready'; import { fetchUserProfile } from '../../lib/pay-wechat'; import { request, toast } from '../../lib/api'; import DeliveryHintHtml from '../../components/DeliveryHintHtml'; @@ -291,9 +290,6 @@ export default function OrderConfirmPage() { } } - const ready = await ensurePayReady(returnPath); - if (!ready) return; - setLoading(true); setMsg(''); try { diff --git a/apps/mini-user/src/pages/product-detail/index.tsx b/apps/mini-user/src/pages/product-detail/index.tsx index d403c1a..03046b5 100644 --- a/apps/mini-user/src/pages/product-detail/index.tsx +++ b/apps/mini-user/src/pages/product-detail/index.tsx @@ -16,7 +16,6 @@ import ProductCarousel from '../../components/ProductCarousel'; import WechatShareReady from '../../components/WechatShareReady'; import BenefitIntroCard from '../../components/BenefitIntroCard'; import { goLogin } from '../../lib/auth-nav'; -import { ensurePayReady } from '../../lib/pay-ready'; import { isLoggedIn, request, toast } from '../../lib/api'; import { getProductCarouselImages, @@ -236,8 +235,6 @@ export default function ProductDetailPage() { goLogin(returnPath); return; } - const ready = await ensurePayReady(returnPath); - if (!ready) return; Taro.navigateTo({ url: returnPath }); } @@ -253,8 +250,6 @@ export default function ProductDetailPage() { goLogin(returnPath); return; } - const ready = await ensurePayReady(returnPath); - if (!ready) return; Taro.navigateTo({ url: returnPath }); } diff --git a/apps/mini-user/src/pages/redeem-success/index.tsx b/apps/mini-user/src/pages/redeem-success/index.tsx index 67015af..d403e27 100644 --- a/apps/mini-user/src/pages/redeem-success/index.tsx +++ b/apps/mini-user/src/pages/redeem-success/index.tsx @@ -57,8 +57,10 @@ function readCachedRecord(): RedeemRecord | null { export default function RedeemSuccessPage() { const router = useRouter(); const [fromHistory, setFromHistory] = useState(false); + const [fromStore, setFromStore] = useState(false); const [record, setRecord] = useState(null); - const [score, setScore] = useState(5); + const [serviceScore, setServiceScore] = useState(5); + const [envScore, setEnvScore] = useState(5); const [tags, setTags] = useState(['菜品好', '环境佳', '服务周到']); const [comment, setComment] = useState(''); const [imageUrls, setImageUrls] = useState([]); @@ -68,8 +70,12 @@ export default function RedeemSuccessPage() { const rated = Boolean(record?.rating); const applyRating = useCallback((rating: StoreRatingDto) => { - const nextScore = Number(rating.serviceScore || rating.envScore || 5); - setScore(Number.isFinite(nextScore) && nextScore > 0 ? Math.min(5, Math.round(nextScore)) : 5); + const nextService = Number(rating.serviceScore || 5); + const nextEnv = Number(rating.envScore || 5); + setServiceScore( + Number.isFinite(nextService) && nextService > 0 ? Math.min(5, Math.round(nextService)) : 5, + ); + setEnvScore(Number.isFinite(nextEnv) && nextEnv > 0 ? Math.min(5, Math.round(nextEnv)) : 5); setTags(Array.isArray(rating.tags) ? rating.tags : []); setComment(String(rating.comment || '')); setImageUrls(Array.isArray(rating.imageUrls) ? rating.imageUrls : []); @@ -105,6 +111,7 @@ export default function RedeemSuccessPage() { const id = String(options?.id || router.params.id || '').trim(); const from = String(options?.from || router.params.from || ''); setFromHistory(from === 'history'); + setFromStore(from === 'store'); if (id) { void loadRecord(id); return; @@ -138,7 +145,7 @@ export default function RedeemSuccessPage() { function leave() { clearCache(); const pages = Taro.getCurrentPages(); - if (fromHistory && pages.length > 1) { + if ((fromHistory || fromStore) && pages.length > 1) { Taro.navigateBack(); return; } @@ -187,8 +194,8 @@ export default function RedeemSuccessPage() { method: 'POST', data: { redeemRecordId: record.id, - serviceScore: score, - envScore: score, + serviceScore, + envScore, comment: comment.trim(), tags, imageUrls, @@ -199,7 +206,7 @@ export default function RedeemSuccessPage() { prev ? { ...prev, - rating: { serviceScore: score, envScore: score, comment, tags, imageUrls }, + rating: { serviceScore, envScore, comment, tags, imageUrls }, } : prev, ); @@ -228,21 +235,39 @@ export default function RedeemSuccessPage() { - 本次到店体验 + 服务评分 {[1, 2, 3, 4, 5].map((value) => ( { - if (!rated) setScore(value); + if (!rated) setServiceScore(value); }} > ★ ))} - {SCORE_LABELS[score]} + {SCORE_LABELS[serviceScore]} + + + + 环境评分 + + {[1, 2, 3, 4, 5].map((value) => ( + { + if (!rated) setEnvScore(value); + }} + > + ★ + + ))} + + {SCORE_LABELS[envScore]} diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx index 2e05d7e..5780198 100644 --- a/apps/mini-user/src/pages/store-detail/index.tsx +++ b/apps/mini-user/src/pages/store-detail/index.tsx @@ -16,7 +16,7 @@ import ProductCarousel from '../../components/ProductCarousel'; import StoreRedeemMarquee, { type StoreRedeemMarqueeItem } from '../../components/StoreRedeemMarquee'; import BenefitIntroCard from '../../components/BenefitIntroCard'; import WechatShareReady from '../../components/WechatShareReady'; -import { request, toast } from '../../lib/api'; +import { request, toast, isLoggedIn } from '../../lib/api'; import { toMoneyNumber } from '../../lib/money'; import { maskPhone, toDialablePhone } from '../../lib/phone'; import { track } from '../../lib/analytics'; @@ -158,7 +158,7 @@ function toMarqueeItem(row: RecentRedeem): StoreRedeemMarqueeItem | null { const userLabel = String(row.userLabel || '用户***').trim() || '用户***'; const amount = formatRedeemAmountYuan(row.amount); if (!amount) return null; - return { userLabel, amount }; + return { userLabel, amount, createdAt: row.createdAt }; } catch { return null; } @@ -189,6 +189,7 @@ export default function StoreDetailPage() { const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [headerSolid, setHeaderSolid] = useState(false); + const [pendingRatingId, setPendingRatingId] = useState(null); const storeRef = useRef(null); storeRef.current = store; @@ -274,6 +275,13 @@ export default function StoreDetailPage() { if (!id) return; void loadStore(id); void loadRecentRedeems(id); + if (isLoggedIn()) { + void request<{ id?: string | null }>(`/redeem/records/pending-rating?storeId=${id}`) + .then((data) => setPendingRatingId(data?.id ? String(data.id) : null)) + .catch(() => setPendingRatingId(null)); + } else { + setPendingRatingId(null); + } }); const sharePayload = useMemo(() => { @@ -530,8 +538,20 @@ export default function StoreDetailPage() { ) : null} + {pendingRatingId ? ( + + Taro.navigateTo({ + url: `/pages/redeem-success/index?id=${pendingRatingId}&from=store`, + }) + } + > + 评价门店 + + ) : null} Taro.navigateTo({ url: '/pages/redeem/index' })} > 到店核销 diff --git a/apps/mini-user/src/pages/store-package-detail/index.config.ts b/apps/mini-user/src/pages/store-package-detail/index.config.ts index 0eeabd1..e90850a 100644 --- a/apps/mini-user/src/pages/store-package-detail/index.config.ts +++ b/apps/mini-user/src/pages/store-package-detail/index.config.ts @@ -1,4 +1,6 @@ export default definePageConfig({ navigationStyle: 'custom', navigationBarTitleText: '套餐详情', -}); \ No newline at end of file + enableShareAppMessage: true, + enableShareTimeline: true, +}); diff --git a/apps/mini-user/src/pages/store-package-detail/index.tsx b/apps/mini-user/src/pages/store-package-detail/index.tsx index 2825b75..9a01164 100644 --- a/apps/mini-user/src/pages/store-package-detail/index.tsx +++ b/apps/mini-user/src/pages/store-package-detail/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { View, Text } from '@tarojs/components'; import '../../styles/store-detail.css'; import '../../styles/product-detail.css'; @@ -7,7 +7,13 @@ import { normalizeStorePackageImageUrls } from '@dukang/shared-types'; import PageShell from '../../components/PageShell'; import PageNavBar from '../../components/PageNavBar'; import ProductCarousel from '../../components/ProductCarousel'; +import WechatShareReady from '../../components/WechatShareReady'; import { request, toast } from '../../lib/api'; +import { + buildSceneSharePayload, + toWeappShareMessage, + toWeappShareTimeline, +} from '../../lib/wechat-share'; type StorePackage = { name: string; @@ -46,6 +52,7 @@ function formatPriceYuan(price: string | number) { export default function StorePackageDetailPage() { const router = useRouter(); const [storeId, setStoreId] = useState(() => pickStoreId(router.params.storeId)); + const [packageIndex, setPackageIndex] = useState(() => parsePackageIndex(router.params.index)); const [storeName, setStoreName] = useState(''); const [pkg, setPkg] = useState(null); const [loading, setLoading] = useState(true); @@ -89,10 +96,34 @@ export default function StorePackageDetailPage() { const sid = pickStoreId(options?.storeId || router.params.storeId); const index = parsePackageIndex(options?.index ?? router.params.index); setStoreId(sid); + setPackageIndex(index); void loadPackage(sid, index); }); - function goBack() { + const imageUrls = useMemo( + () => (pkg ? normalizeStorePackageImageUrls(pkg) : []), + [pkg], + ); + + const sharePayload = useMemo( + () => + buildSceneSharePayload('storeDetail', { + path: `/pages/store-package-detail/index?storeId=${storeId}&index=${packageIndex}`, + dynamicTitle: pkg + ? storeName + ? `${pkg.name} · ${storeName}` + : pkg.name + : undefined, + dynamicDesc: pkg?.dishes?.trim() || undefined, + dynamicImageUrl: imageUrls[0] || undefined, + }), + [imageUrls, pkg, packageIndex, storeId, storeName], + ); + const sharePayloadRef = useRef(sharePayload); + sharePayloadRef.current = sharePayload; + const shareQuery = storeId ? `storeId=${storeId}&index=${packageIndex}` : ''; + + const goBack = useCallback(() => { const pages = Taro.getCurrentPages(); if (pages.length > 1) Taro.navigateBack(); else if (storeId) { @@ -100,30 +131,36 @@ export default function StorePackageDetailPage() { } else { Taro.switchTab({ url: '/pages/stores/index' }); } - } + }, [storeId]); - if (loading) { - return ( - - - 加载中… - - ); - } + // 不用 useShare* Hook:Taro 编译会把它们折进 return,触发 React #310 + useEffect(() => { + const page = Taro.getCurrentInstance().page as + | { + onShareAppMessage?: () => ReturnType; + onShareTimeline?: () => ReturnType; + } + | undefined; + if (!page) return; + page.onShareAppMessage = () => toWeappShareMessage(sharePayloadRef.current); + page.onShareTimeline = () => toWeappShareTimeline(sharePayloadRef.current, shareQuery); + }, [shareQuery]); if (!pkg) { return ( + - {loadError || '套餐不存在'} + + {loading ? '加载中…' : loadError || '套餐不存在'} + ); } - const imageUrls = normalizeStorePackageImageUrls(pkg); - return ( + diff --git a/apps/mini-user/src/pages/stores/index.tsx b/apps/mini-user/src/pages/stores/index.tsx index f671d31..960800f 100644 --- a/apps/mini-user/src/pages/stores/index.tsx +++ b/apps/mini-user/src/pages/stores/index.tsx @@ -492,7 +492,9 @@ export default function StoresPage() { ))} - 核销{s.redeemCount ?? 0}次 + {Number(s.redeemCount) > 0 ? ( + 核销{s.redeemCount}次 + ) : null} diff --git a/server/dukang-api/src/modules/redeem/redeem.controller.ts b/server/dukang-api/src/modules/redeem/redeem.controller.ts index 39b9f4b..0946479 100644 --- a/server/dukang-api/src/modules/redeem/redeem.controller.ts +++ b/server/dukang-api/src/modules/redeem/redeem.controller.ts @@ -51,6 +51,11 @@ export class UserRedeemController { ); } + @Get('records/pending-rating') + pendingRating(@CurrentUser() user: AuthUser, @Query('storeId') storeId: string) { + return this.redeemService.getPendingRatingRecord(user.actorId, storeId); + } + @Get('records/:id') record(@CurrentUser() user: AuthUser, @Param('id') id: string) { return this.redeemService.getUserRecord(user.actorId, id); diff --git a/server/dukang-api/src/modules/redeem/redeem.module.ts b/server/dukang-api/src/modules/redeem/redeem.module.ts index 50bb47f..470b888 100644 --- a/server/dukang-api/src/modules/redeem/redeem.module.ts +++ b/server/dukang-api/src/modules/redeem/redeem.module.ts @@ -3,11 +3,18 @@ import { AnalyticsModule } from '../analytics/analytics.module'; import { IamModule } from '../iam/iam.module'; import { BenefitModule } from '../benefit/benefit.module'; import { SettlementModule } from '../settlement/settlement.module'; +import { StoreModule } from '../store/store.module'; import { RedeemService } from './redeem.service'; import { ShopRedeemController, UserRedeemController } from './redeem.controller'; @Module({ - imports: [IamModule, AnalyticsModule, BenefitModule, forwardRef(() => SettlementModule)], + imports: [ + IamModule, + AnalyticsModule, + BenefitModule, + forwardRef(() => SettlementModule), + forwardRef(() => StoreModule), + ], controllers: [UserRedeemController, ShopRedeemController], providers: [RedeemService], exports: [RedeemService], diff --git a/server/dukang-api/src/modules/redeem/redeem.service.ts b/server/dukang-api/src/modules/redeem/redeem.service.ts index 9603632..978e270 100644 --- a/server/dukang-api/src/modules/redeem/redeem.service.ts +++ b/server/dukang-api/src/modules/redeem/redeem.service.ts @@ -1,7 +1,9 @@ import { BadRequestException, + Inject, Injectable, NotFoundException, + forwardRef, } from '@nestjs/common'; import { randomBytes } from 'crypto'; import { @@ -33,6 +35,7 @@ import { BenefitService } from '../benefit/benefit.service'; import { AuthService } from '../iam/auth.service'; import { PayRedeemAnomalyService } from '../../common/alert/pay-redeem-anomaly.service'; import { WecomMessagePushService } from '../../integrations/wecom/wecom-message-push.service'; +import { StoreService } from '../store/store.service'; type TokenPayload = { userId: string; @@ -123,6 +126,8 @@ export class RedeemService { private readonly payRedeemAnomaly: PayRedeemAnomalyService, private readonly wecomPush: WecomMessagePushService, private readonly systemConfig: SystemConfigService, + @Inject(forwardRef(() => StoreService)) + private readonly storeService: StoreService, ) {} private maskPhoneForStore(phone: string) { @@ -1388,6 +1393,22 @@ export class RedeemService { }); } + async getPendingRatingRecord(userId: bigint, storeId: string) { + const sid = String(storeId || '').trim(); + if (!/^\d+$/.test(sid)) return null; + const record = await this.prisma.redeemRecord.findFirst({ + where: { + userId, + storeId: BigInt(sid), + rating: null, + }, + orderBy: { createdAt: 'desc' }, + select: { id: true }, + }); + if (!record) return null; + return serializeBigInt({ id: record.id }); + } + async submitRating( userId: bigint, body: { @@ -1421,6 +1442,7 @@ export class RedeemService { imageUrls, }, }); + await this.storeService.refreshStoreRatingFromReviews(record.storeId); return serializeBigInt(mapStoreRating(rating)); } } diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts index 20e1644..96264cc 100644 --- a/server/dukang-api/src/modules/store/store.service.ts +++ b/server/dukang-api/src/modules/store/store.service.ts @@ -1668,4 +1668,19 @@ export class StoreService { await this.partnerGetStore(partnerAccountId, storeId); return this.replaceStoreCategories(storeId, categoryIds, priorities); } + + /** 评价提交后回写门店综合评分(服务+环境均值) */ + async refreshStoreRatingFromReviews(storeId: bigint) { + const agg = await this.prisma.storeRating.aggregate({ + where: { storeId }, + _avg: { serviceScore: true, envScore: true }, + }); + const avgService = Number(agg._avg.serviceScore ?? 0); + const avgEnv = Number(agg._avg.envScore ?? 0); + const rating = Math.round(((avgService + avgEnv) / 2) * 100) / 100; + await this.prisma.store.update({ + where: { id: storeId }, + data: { rating }, + }); + } }