客服电话 → 13203801799(CUSTOMER_SERVICE_PHONE,协议文案走常量自动同步)
权益页去掉分享按钮,菜单分享保留 支付成功改 reLaunch 清栈;订单列表返回首页 减数量低于起购时 toast「同城/跨城/现场提货至少购买 N 瓶」 核销成功可评星并 POST /redeem/ratings;HQ 新增「门店评价」页 /store-ratings 成功页「回到首页」改用 .redeem-cancel-btn,不再撑破布局 历史记录改为核销流水:GET /redeem/records;生产库已有 2 笔(用户 13073729990)可验证。权益明细字段映射也一并修了 我的页头像提高点击层级,避免被卡片遮挡
This commit is contained in:
@@ -6,6 +6,7 @@ import DashboardPage from './pages/DashboardPage';
|
|||||||
import UsersPage from './pages/UsersPage';
|
import UsersPage from './pages/UsersPage';
|
||||||
import OrdersPage from './pages/OrdersPage';
|
import OrdersPage from './pages/OrdersPage';
|
||||||
import StoresPage from './pages/StoresPage';
|
import StoresPage from './pages/StoresPage';
|
||||||
|
import StoreRatingsPage from './pages/StoreRatingsPage';
|
||||||
import StoreAccountsPage from './pages/StoreAccountsPage';
|
import StoreAccountsPage from './pages/StoreAccountsPage';
|
||||||
import BenefitCouponsPage from './pages/BenefitCouponsPage';
|
import BenefitCouponsPage from './pages/BenefitCouponsPage';
|
||||||
import BenefitLedgersPage from './pages/BenefitLedgersPage';
|
import BenefitLedgersPage from './pages/BenefitLedgersPage';
|
||||||
@@ -78,6 +79,7 @@ export default function App() {
|
|||||||
<Route path="/products" element={<ProductsPage />} />
|
<Route path="/products" element={<ProductsPage />} />
|
||||||
<Route path="/product-detail-templates" element={<ProductDetailTemplatesPage />} />
|
<Route path="/product-detail-templates" element={<ProductDetailTemplatesPage />} />
|
||||||
<Route path="/stores" element={<StoresPage />} />
|
<Route path="/stores" element={<StoresPage />} />
|
||||||
|
<Route path="/store-ratings" element={<StoreRatingsPage />} />
|
||||||
<Route path="/store-categories" element={<StoreCategoriesPage />} />
|
<Route path="/store-categories" element={<StoreCategoriesPage />} />
|
||||||
<Route path="/store-accounts" element={<StoreAccountsPage />} />
|
<Route path="/store-accounts" element={<StoreAccountsPage />} />
|
||||||
<Route path="/store-media" element={<StoreMediaPage />} />
|
<Route path="/store-media" element={<StoreMediaPage />} />
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const MENU_ITEMS: MenuProps['items'] = [
|
|||||||
label: '门店',
|
label: '门店',
|
||||||
children: [
|
children: [
|
||||||
{ key: '/stores', label: '门店列表' },
|
{ key: '/stores', label: '门店列表' },
|
||||||
|
{ key: '/store-ratings', label: '门店评价' },
|
||||||
{ key: '/store-categories', label: '门店分类' },
|
{ key: '/store-categories', label: '门店分类' },
|
||||||
{ key: '/store-accounts', label: '门店账户' },
|
{ key: '/store-accounts', label: '门店账户' },
|
||||||
{ key: '/store-media', label: '门店资源' },
|
{ key: '/store-media', label: '门店资源' },
|
||||||
@@ -145,6 +146,7 @@ function menuAllowed(key: string, permissionKeys: string[]): boolean {
|
|||||||
'/knowledge-bases': 'knowledge_bases',
|
'/knowledge-bases': 'knowledge_bases',
|
||||||
'stores-group': 'stores',
|
'stores-group': 'stores',
|
||||||
'/stores': 'stores',
|
'/stores': 'stores',
|
||||||
|
'/store-ratings': 'stores',
|
||||||
'/store-categories': 'stores',
|
'/store-categories': 'stores',
|
||||||
'/store-accounts': 'stores',
|
'/store-accounts': 'stores',
|
||||||
'/store-media': 'stores',
|
'/store-media': 'stores',
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, Form, Input, Table, Typography } from 'antd';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { fmtTime } from '../lib/constants';
|
||||||
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
id: string;
|
||||||
|
serviceScore: number;
|
||||||
|
envScore: number;
|
||||||
|
createdAt: string;
|
||||||
|
redeemNo: string;
|
||||||
|
redeemAmount: number;
|
||||||
|
store?: { id: string; name: string; cityName?: string };
|
||||||
|
user?: { userNo: string; phone: string | null; nickname: string | null };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StoreRatingsPage() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const initialStoreId = searchParams.get('storeId') || '';
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [filters, setFilters] = useState<Record<string, string>>({
|
||||||
|
...(initialStoreId ? { storeId: initialStoreId } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, loading, page, pageSize, setPage, setPageSize } = useAdminList<Row>(
|
||||||
|
'/admin/store-ratings',
|
||||||
|
() => {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (filters.storeId) qs.set('storeId', filters.storeId);
|
||||||
|
if (filters.redeemNo) qs.set('redeemNo', filters.redeemNo);
|
||||||
|
return qs;
|
||||||
|
},
|
||||||
|
[filters],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: ColumnsType<Row> = [
|
||||||
|
{ title: '核销号', dataIndex: 'redeemNo', width: 180 },
|
||||||
|
{
|
||||||
|
title: '门店',
|
||||||
|
dataIndex: ['store', 'name'],
|
||||||
|
render: (v, row) => (row.store?.cityName ? `${v}(${row.store.cityName})` : v),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户',
|
||||||
|
width: 140,
|
||||||
|
render: (_, row) => row.user?.phone || row.user?.userNo || '-',
|
||||||
|
},
|
||||||
|
{ title: '核销额', dataIndex: 'redeemAmount', width: 90, render: (v) => `¥${v}` },
|
||||||
|
{ title: '服务分', dataIndex: 'serviceScore', width: 80 },
|
||||||
|
{ title: '环境分', dataIndex: 'envScore', width: 80 },
|
||||||
|
{ title: '评价时间', dataIndex: 'createdAt', width: 170, render: fmtTime },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||||
|
门店评价
|
||||||
|
</Typography.Title>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="inline"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
initialValues={filters}
|
||||||
|
onFinish={(v) => {
|
||||||
|
setFilters({
|
||||||
|
storeId: v.storeId || '',
|
||||||
|
redeemNo: v.redeemNo || '',
|
||||||
|
});
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item name="storeId" label="门店ID">
|
||||||
|
<Input allowClear placeholder="storeId" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="redeemNo" label="核销号">
|
||||||
|
<Input allowClear />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.items ?? []}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
onChange: (p, ps) => {
|
||||||
|
setPage(p);
|
||||||
|
setPageSize(ps);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -770,9 +770,12 @@ export default function StoresPage() {
|
|||||||
{ title: '店长', dataIndex: ['account', 'name'], width: 90, render: (v) => v || '—' },
|
{ title: '店长', dataIndex: ['account', 'name'], width: 90, render: (v) => v || '—' },
|
||||||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
{
|
{
|
||||||
title: '操作', width: 80,
|
title: '操作', width: 140,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Button type="link" size="small" onClick={() => void openStoreDetail(row)}>详情</Button>
|
<Space size={0}>
|
||||||
|
<Button type="link" size="small" onClick={() => void openStoreDetail(row)}>详情</Button>
|
||||||
|
<Button type="link" size="small" onClick={() => navigate(`/store-ratings?storeId=${row.id}`)}>评价</Button>
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { View, Text, Image } from '@tarojs/components';
|
import { View, Text, Image } from '@tarojs/components';
|
||||||
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import ShareNavButton from '../../components/ShareNavButton';
|
|
||||||
import WechatShareReady from '../../components/WechatShareReady';
|
import WechatShareReady from '../../components/WechatShareReady';
|
||||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||||
import { goLogin } from '../../lib/auth-nav';
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
@@ -31,6 +30,14 @@ type CouponItem = {
|
|||||||
sourceProduct: string;
|
sourceProduct: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RedeemHistoryItem = {
|
||||||
|
id: string;
|
||||||
|
redeemNo: string;
|
||||||
|
amount: number;
|
||||||
|
storeName: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
function formatMoney(amount: number) {
|
function formatMoney(amount: number) {
|
||||||
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
@@ -46,11 +53,13 @@ export default function BenefitPage() {
|
|||||||
const [loggedIn, setLoggedIn] = useState(() => isLoggedIn());
|
const [loggedIn, setLoggedIn] = useState(() => isLoggedIn());
|
||||||
const [summary, setSummary] = useState<BenefitSummary | null>(null);
|
const [summary, setSummary] = useState<BenefitSummary | null>(null);
|
||||||
const [coupons, setCoupons] = useState<CouponItem[]>([]);
|
const [coupons, setCoupons] = useState<CouponItem[]>([]);
|
||||||
|
const [redeemHistory, setRedeemHistory] = useState<RedeemHistoryItem[]>([]);
|
||||||
const [tab, setTab] = useState<'available' | 'history'>('available');
|
const [tab, setTab] = useState<'available' | 'history'>('available');
|
||||||
|
|
||||||
const resetGuestState = useCallback(() => {
|
const resetGuestState = useCallback(() => {
|
||||||
setSummary(null);
|
setSummary(null);
|
||||||
setCoupons([]);
|
setCoupons([]);
|
||||||
|
setRedeemHistory([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadBenefit = useCallback(() => {
|
const loadBenefit = useCallback(() => {
|
||||||
@@ -58,10 +67,17 @@ export default function BenefitPage() {
|
|||||||
return Promise.all([
|
return Promise.all([
|
||||||
request<BenefitSummary>('/benefit/summary'),
|
request<BenefitSummary>('/benefit/summary'),
|
||||||
request<CouponItem[]>('/benefit/coupons'),
|
request<CouponItem[]>('/benefit/coupons'),
|
||||||
|
request<{ list?: RedeemHistoryItem[] } | RedeemHistoryItem[]>('/redeem/records?page=1&pageSize=50'),
|
||||||
])
|
])
|
||||||
.then(([s, list]) => {
|
.then(([s, list, records]) => {
|
||||||
setSummary(s);
|
setSummary(s);
|
||||||
setCoupons(Array.isArray(list) ? list : []);
|
setCoupons(Array.isArray(list) ? list : []);
|
||||||
|
const hist = Array.isArray(records)
|
||||||
|
? records
|
||||||
|
: Array.isArray(records?.list)
|
||||||
|
? records.list
|
||||||
|
: [];
|
||||||
|
setRedeemHistory(hist);
|
||||||
})
|
})
|
||||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -89,8 +105,6 @@ export default function BenefitPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
|
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
|
||||||
const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID');
|
|
||||||
const visible = tab === 'available' ? available : history;
|
|
||||||
|
|
||||||
const sharePayload = useMemo(
|
const sharePayload = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -123,9 +137,6 @@ export default function BenefitPage() {
|
|||||||
<View className="benefit-header-city-pin" />
|
<View className="benefit-header-city-pin" />
|
||||||
<Text>郑州市</Text>
|
<Text>郑州市</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="benefit-header__share page-nav-bar__right-slot">
|
|
||||||
<ShareNavButton payload={sharePayload} />
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -180,26 +191,26 @@ export default function BenefitPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{visible.length === 0 ? (
|
{tab === 'available' ? (
|
||||||
<View className="u-empty">{tab === 'available' ? '暂无可用权益' : '暂无历史记录'}</View>
|
available.length === 0 ? (
|
||||||
) : (
|
<View className="u-empty">暂无可用权益</View>
|
||||||
visible.map((c) => (
|
) : (
|
||||||
<View key={c.id} className="benefit-coupon">
|
available.map((c) => (
|
||||||
<View className="benefit-coupon-notch" />
|
<View key={c.id} className="benefit-coupon">
|
||||||
<View className="benefit-coupon-notch benefit-coupon-notch--right" />
|
<View className="benefit-coupon-notch" />
|
||||||
<View className="benefit-coupon-head">
|
<View className="benefit-coupon-notch benefit-coupon-notch--right" />
|
||||||
<Text className="benefit-coupon-name">{c.sourceProduct || '好客权益'}</Text>
|
<View className="benefit-coupon-head">
|
||||||
<Text className="benefit-coupon-balance">¥{formatMoney(c.balance)}</Text>
|
<Text className="benefit-coupon-name">{c.sourceProduct || '好客权益'}</Text>
|
||||||
</View>
|
<Text className="benefit-coupon-balance">¥{formatMoney(c.balance)}</Text>
|
||||||
<Text className="benefit-coupon-no">NO. {c.couponNo}</Text>
|
</View>
|
||||||
<View className="benefit-progress">
|
<Text className="benefit-coupon-no">NO. {c.couponNo}</Text>
|
||||||
<View className="benefit-progress-bar" style={{ width: `${usagePercent(c)}%` }} />
|
<View className="benefit-progress">
|
||||||
</View>
|
<View className="benefit-progress-bar" style={{ width: `${usagePercent(c)}%` }} />
|
||||||
<View className="benefit-coupon-footer">
|
</View>
|
||||||
<Text className="benefit-coupon-meta">
|
<View className="benefit-coupon-footer">
|
||||||
已用 ¥{formatMoney(c.usedAmount)} / 总额 ¥{formatMoney(c.totalAmount)}
|
<Text className="benefit-coupon-meta">
|
||||||
</Text>
|
已用 ¥{formatMoney(c.usedAmount)} / 总额 ¥{formatMoney(c.totalAmount)}
|
||||||
{tab === 'available' ? (
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
className="benefit-coupon-btn"
|
className="benefit-coupon-btn"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -210,7 +221,26 @@ export default function BenefitPage() {
|
|||||||
>
|
>
|
||||||
立即核销
|
立即核销
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
</View>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
) : redeemHistory.length === 0 ? (
|
||||||
|
<View className="u-empty">暂无核销记录</View>
|
||||||
|
) : (
|
||||||
|
redeemHistory.map((r) => (
|
||||||
|
<View key={r.id} className="benefit-coupon">
|
||||||
|
<View className="benefit-coupon-notch" />
|
||||||
|
<View className="benefit-coupon-notch benefit-coupon-notch--right" />
|
||||||
|
<View className="benefit-coupon-head">
|
||||||
|
<Text className="benefit-coupon-name">{r.storeName || '门店核销'}</Text>
|
||||||
|
<Text className="benefit-coupon-balance">-¥{formatMoney(Number(r.amount))}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="benefit-coupon-no">NO. {r.redeemNo}</Text>
|
||||||
|
<View className="benefit-coupon-footer">
|
||||||
|
<Text className="benefit-coupon-meta">
|
||||||
|
{r.createdAt ? String(r.createdAt).slice(0, 19).replace('T', ' ') : '-'}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -221,7 +221,10 @@ export default function MinePage() {
|
|||||||
|
|
||||||
/** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */
|
/** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */
|
||||||
async function handleAvatarTap() {
|
async function handleAvatarTap() {
|
||||||
if (bindingWx || savingProfile) return;
|
if (bindingWx || savingProfile) {
|
||||||
|
toast(savingProfile ? '资料保存中…' : '请稍候…');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
goLogin('/pages/mine/index');
|
goLogin('/pages/mine/index');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { goLogin } from '../../lib/auth-nav';
|
|||||||
import { buildPayUrl } from '../../lib/checkout-nav';
|
import { buildPayUrl } from '../../lib/checkout-nav';
|
||||||
import { ensurePayReady } from '../../lib/pay-ready';
|
import { ensurePayReady } from '../../lib/pay-ready';
|
||||||
import { fetchUserProfile } from '../../lib/pay-wechat';
|
import { fetchUserProfile } from '../../lib/pay-wechat';
|
||||||
import { request } from '../../lib/api';
|
import { request, toast } from '../../lib/api';
|
||||||
import { getProductMainImage } from '../../lib/product-images';
|
import { getProductMainImage } from '../../lib/product-images';
|
||||||
|
|
||||||
type PreviewProduct = {
|
type PreviewProduct = {
|
||||||
@@ -75,7 +75,13 @@ export default function OrderConfirmPickupPage() {
|
|||||||
const canSubmit = !!preview && quantityOk && !loading && !previewLoading;
|
const canSubmit = !!preview && quantityOk && !loading && !previewLoading;
|
||||||
|
|
||||||
function updateQuantity(next: number) {
|
function updateQuantity(next: number) {
|
||||||
if (next < minQty) return;
|
if (next < 1) return;
|
||||||
|
if (next < minQty) {
|
||||||
|
const tip = `现场提货至少购买 ${minQty} 瓶`;
|
||||||
|
toast(tip);
|
||||||
|
setMsg(tip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setQuantity(next);
|
setQuantity(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,15 @@ export default function OrderConfirmPage() {
|
|||||||
|
|
||||||
function updateQuantity(next: number) {
|
function updateQuantity(next: number) {
|
||||||
if (next < 1) return;
|
if (next < 1) return;
|
||||||
|
if (next < minQty) {
|
||||||
|
const tip = isCross
|
||||||
|
? `跨城配送至少购买 ${minQty} 瓶(1箱)`
|
||||||
|
: `同城配送至少购买 ${minQty} 瓶`;
|
||||||
|
toast(tip);
|
||||||
|
setMsg(tip);
|
||||||
|
setQuantity(Math.max(1, next));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setQuantity(next);
|
setQuantity(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,11 +220,11 @@ export default function OrderConfirmPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!quantityOk) {
|
if (!quantityOk) {
|
||||||
setMsg(
|
const tip = isCross
|
||||||
isCross
|
? `跨城配送至少购买 ${minQty} 瓶(1箱)`
|
||||||
? `跨城配送至少购买 ${minQty} 瓶(1箱)`
|
: `同城配送至少购买 ${minQty} 瓶`;
|
||||||
: `同城配送至少购买 ${minQty} 瓶`,
|
setMsg(tip);
|
||||||
);
|
toast(tip);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,14 @@ export default function OrderDetailPage() {
|
|||||||
<WechatShareReady payload={sharePayload} />
|
<WechatShareReady payload={sharePayload} />
|
||||||
<SubPageHeader
|
<SubPageHeader
|
||||||
title="订单详情"
|
title="订单详情"
|
||||||
|
onBack={() => {
|
||||||
|
const pages = Taro.getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
Taro.navigateBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Taro.switchTab({ url: '/pages/home/index' });
|
||||||
|
}}
|
||||||
right={<ShareNavButton payload={sharePayload} />}
|
right={<ShareNavButton payload={sharePayload} />}
|
||||||
/>
|
/>
|
||||||
<View className="sub-page-body">
|
<View className="sub-page-body">
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ export default function OrdersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="sub" className="orders-page">
|
<PageShell variant="sub" className="orders-page">
|
||||||
<SubPageHeader title="我的订单" />
|
<SubPageHeader
|
||||||
|
title="我的订单"
|
||||||
|
onBack={() => Taro.switchTab({ url: '/pages/home/index' })}
|
||||||
|
/>
|
||||||
<View className="order-tabs">
|
<View className="order-tabs">
|
||||||
{TABS.map((t) => (
|
{TABS.map((t) => (
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -144,10 +144,10 @@ export default function PayPage() {
|
|||||||
toast('支付成功', 'success');
|
toast('支付成功', 'success');
|
||||||
}
|
}
|
||||||
if (deliveryType === 'ON_SITE_PICKUP') {
|
if (deliveryType === 'ON_SITE_PICKUP') {
|
||||||
// 现场提货支付即完成 → 订单详情(已完成)
|
// 现场提货支付即完成 → 订单详情(已完成);reLaunch 清掉商品详情栈
|
||||||
Taro.redirectTo({ url: `/pages/order-detail/index?id=${orderId}` });
|
Taro.reLaunch({ url: `/pages/order-detail/index?id=${orderId}` });
|
||||||
} else {
|
} else {
|
||||||
Taro.redirectTo({ url: '/pages/orders/index?tab=paid' });
|
Taro.reLaunch({ url: '/pages/orders/index?tab=paid' });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isWechatAuthRequiredError(e)) {
|
if (isWechatAuthRequiredError(e)) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro, { useRouter } from '@tarojs/taro';
|
import Taro, { useRouter } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import SubPageHeader from '../../components/SubPageHeader';
|
import SubPageHeader from '../../components/SubPageHeader';
|
||||||
|
import { request, toast } from '../../lib/api';
|
||||||
|
|
||||||
const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult';
|
const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult';
|
||||||
|
|
||||||
@@ -19,8 +20,38 @@ function formatMoney(amount: number) {
|
|||||||
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StarRating({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
onChange: (score: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View className="redeem-rating-row">
|
||||||
|
<Text className="redeem-rating-label">{label}</Text>
|
||||||
|
<View className="redeem-star-row">
|
||||||
|
{[1, 2, 3, 4, 5].map((score) => (
|
||||||
|
<Text
|
||||||
|
key={score}
|
||||||
|
className={`redeem-star-btn${score <= value ? ' redeem-star-btn--active' : ''}`}
|
||||||
|
onClick={() => onChange(score)}
|
||||||
|
>
|
||||||
|
★
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function RedeemSuccessPage() {
|
export default function RedeemSuccessPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serviceScore, setServiceScore] = useState(5);
|
||||||
|
const [envScore, setEnvScore] = useState(5);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const record = useMemo<RedeemRecord | null>(() => {
|
const record = useMemo<RedeemRecord | null>(() => {
|
||||||
try {
|
try {
|
||||||
@@ -38,14 +69,49 @@ export default function RedeemSuccessPage() {
|
|||||||
? new Date(record.createdAt).toLocaleString('zh-CN')
|
? new Date(record.createdAt).toLocaleString('zh-CN')
|
||||||
: new Date().toLocaleString('zh-CN');
|
: new Date().toLocaleString('zh-CN');
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
try {
|
||||||
|
Taro.removeStorageSync(LAST_REDEEM_RESULT_KEY);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function goBenefit() {
|
function goBenefit() {
|
||||||
Taro.removeStorageSync(LAST_REDEEM_RESULT_KEY);
|
clearCache();
|
||||||
Taro.switchTab({ url: '/pages/benefit/index' });
|
Taro.switchTab({ url: '/pages/benefit/index' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
clearCache();
|
||||||
|
Taro.switchTab({ url: '/pages/home/index' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRatingAndFinish() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (record?.id) {
|
||||||
|
await request('/redeem/ratings', {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
redeemRecordId: record.id,
|
||||||
|
serviceScore,
|
||||||
|
envScore,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
toast('评价已提交', 'success');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 评价失败不阻塞返回 */
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
goBenefit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="sub" className="redeem-success-page">
|
<PageShell variant="sub" className="redeem-success-page">
|
||||||
<SubPageHeader title="核销成功" />
|
<SubPageHeader title="核销成功" onBack={goBenefit} />
|
||||||
<View className="sub-page-body">
|
<View className="sub-page-body">
|
||||||
<View className="redeem-success-icon">
|
<View className="redeem-success-icon">
|
||||||
<Text>✓</Text>
|
<Text>✓</Text>
|
||||||
@@ -69,14 +135,21 @@ export default function RedeemSuccessPage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="redeem-submit" onClick={goBenefit}>
|
<View className="redeem-success-rating">
|
||||||
<Text>返回权益</Text>
|
<Text className="redeem-success-rating-title">为门店服务评分</Text>
|
||||||
|
<StarRating label="服务态度" value={serviceScore} onChange={setServiceScore} />
|
||||||
|
<StarRating label="用餐环境" value={envScore} onChange={setEnvScore} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
className="u-btn u-btn--ghost u-btn--block"
|
className={`redeem-submit${loading ? ' redeem-submit--disabled' : ''}`}
|
||||||
style={{ margin: '0 20px', boxSizing: 'border-box' }}
|
onClick={() => {
|
||||||
onClick={() => Taro.switchTab({ url: '/pages/home/index' })}
|
if (!loading) void submitRatingAndFinish();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Text>{loading ? '提交中…' : '提交评价并返回'}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="redeem-cancel-btn" onClick={goHome}>
|
||||||
<Text>回到首页</Text>
|
<Text>回到首页</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
|
|
||||||
.mine-header {
|
.mine-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
padding: 16px var(--space-page) 44px;
|
padding: 16px var(--space-page) 44px;
|
||||||
background: linear-gradient(135deg, #820012 0%, var(--color-heritage-red) 40%, #d4a373 100%);
|
background: linear-gradient(135deg, #820012 0%, var(--color-heritage-red) 40%, #d4a373 100%);
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-header-texture {
|
.mine-header-texture {
|
||||||
@@ -33,11 +34,12 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-avatar-wrap {
|
.mine-avatar-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 4;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
@@ -160,7 +162,7 @@
|
|||||||
.mine-main {
|
.mine-main {
|
||||||
margin-top: -28px;
|
margin-top: -28px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
padding: 0 var(--space-page) 0;
|
padding: 0 var(--space-page) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -378,6 +378,49 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.redeem-success-rating {
|
||||||
|
margin: 16px var(--space-page) 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-success-rating-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-rating-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-rating-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-star-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-star-btn {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #d0d0d0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeem-star-btn--active {
|
||||||
|
color: #f5a623;
|
||||||
|
}
|
||||||
|
|
||||||
.ledger-item {
|
.ledger-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const MINI_USER_STATIC_OSS_BASE =
|
|||||||
export const QUALIFICATION_DISCLOSURE_URL = `${MINI_USER_STATIC_OSS_BASE}qualification-disclosure.png`;
|
export const QUALIFICATION_DISCLOSURE_URL = `${MINI_USER_STATIC_OSS_BASE}qualification-disclosure.png`;
|
||||||
|
|
||||||
/** 总部客服电话(C 端联系客服) */
|
/** 总部客服电话(C 端联系客服) */
|
||||||
export const CUSTOMER_SERVICE_PHONE = '400-888-1234';
|
export const CUSTOMER_SERVICE_PHONE = '13203801799';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企业微信「微信客服」链接(C 端「在线客服」;微信内网页点击后进入原生客服会话)
|
* 企业微信「微信客服」链接(C 端「在线客服」;微信内网页点击后进入原生客服会话)
|
||||||
|
|||||||
@@ -119,7 +119,29 @@ export class BenefitService {
|
|||||||
where: benefitLedgerWhere(userId, couponId),
|
where: benefitLedgerWhere(userId, couponId),
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
return serializeBigInt(list);
|
return serializeBigInt(
|
||||||
|
list.map((e) => {
|
||||||
|
const type = e.param1 || '';
|
||||||
|
const amount = Number(e.amount1 ?? 0);
|
||||||
|
const title =
|
||||||
|
e.remark ||
|
||||||
|
(type === 'REDEEM'
|
||||||
|
? '门店核销'
|
||||||
|
: type === 'GRANT'
|
||||||
|
? '购酒入账'
|
||||||
|
: type === 'REFUND_VOID'
|
||||||
|
? '退款作废'
|
||||||
|
: '权益变动');
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
type,
|
||||||
|
amount,
|
||||||
|
title,
|
||||||
|
createdAt: e.createdAt,
|
||||||
|
balanceAfter: e.amount2 != null ? Number(e.amount2) : null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoupon(userId: bigint, couponId: bigint) {
|
async getCoupon(userId: bigint, couponId: bigint) {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
|
import { AdminStoreRatingsService } from './admin-store-ratings.service';
|
||||||
|
import { AdminStoreRatingsQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
|
@Controller('admin/store-ratings')
|
||||||
|
@UseGuards(HqAuthGuard)
|
||||||
|
export class AdminStoreRatingsController {
|
||||||
|
constructor(private readonly service: AdminStoreRatingsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(@Query() query: AdminStoreRatingsQueryDto) {
|
||||||
|
return this.service.list(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
|
import type { AdminStoreRatingsQueryDto } from './dto/admin-query.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminStoreRatingsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async list(query: AdminStoreRatingsQueryDto) {
|
||||||
|
const page = query.page ?? 1;
|
||||||
|
const pageSize = query.pageSize ?? 20;
|
||||||
|
const where: Prisma.StoreRatingWhereInput = {};
|
||||||
|
if (query.storeId) where.storeId = BigInt(query.storeId);
|
||||||
|
if (query.redeemNo) {
|
||||||
|
where.redeemRecord = { redeemNo: { contains: query.redeemNo } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
this.prisma.storeRating.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
include: {
|
||||||
|
store: { select: { id: true, name: true, cityName: true } },
|
||||||
|
redeemRecord: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
redeemNo: true,
|
||||||
|
amount: true,
|
||||||
|
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.storeRating.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return serializeBigInt({
|
||||||
|
items: items.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
serviceScore: r.serviceScore,
|
||||||
|
envScore: r.envScore,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
store: r.store,
|
||||||
|
redeemRecordId: r.redeemRecordId,
|
||||||
|
redeemNo: r.redeemRecord.redeemNo,
|
||||||
|
redeemAmount: Number(r.redeemRecord.amount),
|
||||||
|
user: r.redeemRecord.user,
|
||||||
|
})),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,6 +244,16 @@ export class AdminRedeemRecordsQueryDto extends PaginationQueryDto {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminStoreRatingsQueryDto extends PaginationQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
storeId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
redeemNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class AdminDeliveriesQueryDto extends PaginationQueryDto {
|
export class AdminDeliveriesQueryDto extends PaginationQueryDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { AdminBenefitCouponsController, AdminBenefitLedgersController } from './
|
|||||||
import { AdminBenefitService } from './admin-benefit.service';
|
import { AdminBenefitService } from './admin-benefit.service';
|
||||||
import { AdminRedeemRecordsController, AdminDeliveriesController } from './admin-redeem.controller';
|
import { AdminRedeemRecordsController, AdminDeliveriesController } from './admin-redeem.controller';
|
||||||
import { AdminRedeemService, AdminDeliveriesService } from './admin-redeem.service';
|
import { AdminRedeemService, AdminDeliveriesService } from './admin-redeem.service';
|
||||||
|
import { AdminStoreRatingsController } from './admin-store-ratings.controller';
|
||||||
|
import { AdminStoreRatingsService } from './admin-store-ratings.service';
|
||||||
import { AdminHqAccountsController } from './admin-hq-accounts.controller';
|
import { AdminHqAccountsController } from './admin-hq-accounts.controller';
|
||||||
import { AdminHqAccountsService } from './admin-hq-accounts.service';
|
import { AdminHqAccountsService } from './admin-hq-accounts.service';
|
||||||
import { AdminProductsController } from './admin-products.controller';
|
import { AdminProductsController } from './admin-products.controller';
|
||||||
@@ -90,6 +92,7 @@ import { AdminFulfillmentProvidersController } from './admin-fulfillment-provide
|
|||||||
AdminBenefitCouponsController,
|
AdminBenefitCouponsController,
|
||||||
AdminBenefitLedgersController,
|
AdminBenefitLedgersController,
|
||||||
AdminRedeemRecordsController,
|
AdminRedeemRecordsController,
|
||||||
|
AdminStoreRatingsController,
|
||||||
AdminDeliveriesController,
|
AdminDeliveriesController,
|
||||||
AdminHqAccountsController,
|
AdminHqAccountsController,
|
||||||
AdminProductsController,
|
AdminProductsController,
|
||||||
@@ -122,6 +125,7 @@ import { AdminFulfillmentProvidersController } from './admin-fulfillment-provide
|
|||||||
AdminCitiesService,
|
AdminCitiesService,
|
||||||
AdminBenefitService,
|
AdminBenefitService,
|
||||||
AdminRedeemService,
|
AdminRedeemService,
|
||||||
|
AdminStoreRatingsService,
|
||||||
AdminDeliveriesService,
|
AdminDeliveriesService,
|
||||||
AdminHqAccountsService,
|
AdminHqAccountsService,
|
||||||
AdminProductsService,
|
AdminProductsService,
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ export class UserRedeemController {
|
|||||||
rating(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>) {
|
rating(@CurrentUser() user: AuthUser, @Body() body: Record<string, unknown>) {
|
||||||
return this.redeemService.submitRating(user.actorId, body as never);
|
return this.redeemService.submitRating(user.actorId, body as never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('records')
|
||||||
|
records(
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
@Query('page') page = '1',
|
||||||
|
@Query('pageSize') pageSize = '20',
|
||||||
|
) {
|
||||||
|
return this.redeemService.listUserRecords(
|
||||||
|
user.actorId,
|
||||||
|
Number(page),
|
||||||
|
Number(pageSize),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('shop/redeem')
|
@Controller('shop/redeem')
|
||||||
|
|||||||
@@ -1095,11 +1095,51 @@ export class RedeemService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listUserRecords(userId: bigint, page = 1, pageSize = 20) {
|
||||||
|
const take = Math.min(Math.max(pageSize, 1), 50);
|
||||||
|
const skip = (Math.max(page, 1) - 1) * take;
|
||||||
|
const [list, total] = await Promise.all([
|
||||||
|
this.prisma.redeemRecord.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
include: {
|
||||||
|
store: { select: { id: true, name: true } },
|
||||||
|
rating: { select: { serviceScore: true, envScore: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.redeemRecord.count({ where: { userId } }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
list: serializeBigInt(
|
||||||
|
list.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
redeemNo: r.redeemNo,
|
||||||
|
amount: Number(r.amount),
|
||||||
|
storeId: r.storeId,
|
||||||
|
storeName: r.store?.name ?? '门店',
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
rating: r.rating
|
||||||
|
? { serviceScore: r.rating.serviceScore, envScore: r.rating.envScore }
|
||||||
|
: null,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
total,
|
||||||
|
page: Math.max(page, 1),
|
||||||
|
pageSize: take,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async submitRating(userId: bigint, body: { redeemRecordId: string; serviceScore: number; envScore: number }) {
|
async submitRating(userId: bigint, body: { redeemRecordId: string; serviceScore: number; envScore: number }) {
|
||||||
const record = await this.prisma.redeemRecord.findFirst({
|
const record = await this.prisma.redeemRecord.findFirst({
|
||||||
where: { id: BigInt(body.redeemRecordId), userId },
|
where: { id: BigInt(body.redeemRecordId), userId },
|
||||||
});
|
});
|
||||||
if (!record) throw new NotFoundException('核销记录不存在');
|
if (!record) throw new NotFoundException('核销记录不存在');
|
||||||
|
const existing = await this.prisma.storeRating.findUnique({
|
||||||
|
where: { redeemRecordId: record.id },
|
||||||
|
});
|
||||||
|
if (existing) return serializeBigInt(existing);
|
||||||
const rating = await this.prisma.storeRating.create({
|
const rating = await this.prisma.storeRating.create({
|
||||||
data: {
|
data: {
|
||||||
redeemRecordId: record.id,
|
redeemRecordId: record.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user