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 },
+ });
+ }
}