fix: 修复订单管理功能

This commit is contained in:
richarjiang
2026-04-12 18:16:18 +08:00
parent 0810f71250
commit 9639f44698
4 changed files with 78 additions and 20 deletions

37
AGENTS.md Normal file
View File

@@ -0,0 +1,37 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository is a `pnpm` workspace with three packages under `packages/`:
- `packages/app`: Vue 3 + `uni-app` WeChat mini-program. Pages live in `src/pages`, shared UI in `src/components`, state in `src/stores`, and utilities in `src/utils`.
- `packages/server`: NestJS API with Prisma. Feature modules live in `src/<domain>`, unit tests are colocated in `__tests__`, and the database schema and seed script are in `prisma/`.
- `packages/shared`: shared TypeScript enums, constants, and types used by both app and server.
Reference docs and implementation notes are under `docs/`.
## Build, Test, and Development Commands
- `pnpm dev:server`: start the NestJS API with watch mode.
- `pnpm dev:app`: build and serve the `uni-app` target for WeChat mini-program development.
- `pnpm build:shared`: compile shared types before consuming package changes.
- `pnpm build:server`: build the backend into `packages/server/dist`.
- `pnpm build:app`: produce the WeChat mini-program build.
- `pnpm test`: run workspace tests; today this mainly executes server Jest tests.
- `pnpm lint`: run workspace linting; currently defined for the server.
For Prisma tasks, work in `packages/server`: `pnpm prisma:generate`, `pnpm prisma:migrate`, `pnpm prisma:seed`.
## Coding Style & Naming Conventions
Use TypeScript throughout. Follow the existing style: 2-space indentation in JSON/Markdown, `camelCase` for variables/functions, `PascalCase` for Vue components and NestJS classes, and `kebab-case` for page/component filenames such as `flash-sales.vue`. Keep modules feature-oriented and prefer colocating DTOs and tests with their domain module.
Linting is configured in the server via ESLint. The app relies on `vue-tsc` for type checks: run `pnpm --filter @mp-pilates/app type-check` before shipping UI changes.
## Testing Guidelines
Server tests use Jest with `*.spec.ts` naming. Place tests in `packages/server/src/**/__tests__/` and focus on service-level behavior and edge cases around booking, membership, payment, and scheduling logic. Run `pnpm test` for the full suite or `pnpm --filter @mp-pilates/server test:cov` when touching business-critical paths.
## Commit & Pull Request Guidelines
Recent history uses Conventional Commit prefixes such as `feat:`, `fix:`, `fix(app):`, and `perf:`. Keep subjects short and specific, preferably describing the user-visible effect.
Pull requests should include a concise summary, linked issue or task reference, test notes, and screenshots or recordings for mini-program UI changes. Call out Prisma schema changes, new environment variables, or deployment steps explicitly.
## Security & Configuration Tips
Do not commit real secrets. Review `packages/server/certs/` and environment-specific payment or WeChat credentials carefully before pushing. When changing shared types or enums, update both consumers and rebuild `@mp-pilates/shared` to avoid runtime drift.

View File

@@ -44,7 +44,9 @@
class="list-scroll" class="list-scroll"
:refresher-enabled="true" :refresher-enabled="true"
:refresher-triggered="refreshing" :refresher-triggered="refreshing"
:lower-threshold="120"
@refresherrefresh="onRefresh" @refresherrefresh="onRefresh"
@scrolltolower="loadMore"
> >
<!-- Loading skeleton --> <!-- Loading skeleton -->
<view v-if="loading && !orders.length" class="order-list"> <view v-if="loading && !orders.length" class="order-list">
@@ -120,7 +122,7 @@
</view> </view>
<!-- Load more / no more --> <!-- Load more / no more -->
<view v-if="hasMore" class="load-more" @tap="loadMore"> <view v-if="hasMore" class="load-more">
<text class="load-more-text">{{ loading ? '加载中...' : '加载更多' }}</text> <text class="load-more-text">{{ loading ? '加载中...' : '加载更多' }}</text>
</view> </view>
<view v-else-if="orders.length > 0" class="no-more"> <view v-else-if="orders.length > 0" class="no-more">

View File

@@ -20,6 +20,33 @@ import type {
UpdateFlashSaleDto, UpdateFlashSaleDto,
} from '@mp-pilates/shared' } from '@mp-pilates/shared'
interface LegacyPaginatedData<T> {
readonly data: readonly T[]
readonly total: number
readonly page: number
readonly limit: number
}
function normalizePaginatedData<T>(
result: PaginatedData<T> | LegacyPaginatedData<T>,
): PaginatedData<T> {
if ('items' in result) {
return {
items: [...result.items],
total: result.total,
page: result.page,
limit: result.limit,
}
}
return {
items: [...result.data],
total: result.total,
page: result.page,
limit: result.limit,
}
}
export interface AdminStats { export interface AdminStats {
todayBookings: number todayBookings: number
totalOrders: number totalOrders: number
@@ -82,13 +109,13 @@ export const useAdminStore = defineStore('admin', () => {
} }
async function createCardType(dto: CreateCardTypeDto): Promise<CardType> { async function createCardType(dto: CreateCardTypeDto): Promise<CardType> {
const data = await post<CardType>('/admin/card-types', dto) const data = await post<CardType>('/admin/card-types', dto as unknown as Record<string, unknown>)
await fetchCardTypes() await fetchCardTypes()
return data return data
} }
async function updateCardType(id: string, dto: UpdateCardTypeDto): Promise<CardType> { async function updateCardType(id: string, dto: UpdateCardTypeDto): Promise<CardType> {
const data = await put<CardType>(`/admin/card-types/${id}`, dto) const data = await put<CardType>(`/admin/card-types/${id}`, dto as unknown as Record<string, unknown>)
await fetchCardTypes() await fetchCardTypes()
return data return data
} }
@@ -109,7 +136,7 @@ export const useAdminStore = defineStore('admin', () => {
} }
async function saveStudioConfig(dto: UpdateStudioConfigDto): Promise<StudioConfig> { async function saveStudioConfig(dto: UpdateStudioConfigDto): Promise<StudioConfig> {
const data = await put<StudioConfig>('/admin/studio/info', dto) const data = await put<StudioConfig>('/admin/studio/info', dto as unknown as Record<string, unknown>)
studioConfig.value = data studioConfig.value = data
return data return data
} }
@@ -120,10 +147,11 @@ export const useAdminStore = defineStore('admin', () => {
limit?: number limit?: number
status?: string status?: string
}): Promise<PaginatedData<OrderWithDetails>> { }): Promise<PaginatedData<OrderWithDetails>> {
console.log('[admin] fetchAdminOrders params:', params) const result = await get<PaginatedData<OrderWithDetails> | LegacyPaginatedData<OrderWithDetails>>(
const result = await get<PaginatedData<OrderWithDetails>>('/admin/orders', params) '/admin/orders',
console.log('[admin] fetchAdminOrders result:', result) params,
return result )
return normalizePaginatedData(result)
} }
// ── Bookings ───────────────────────────────────────────────────── // ── Bookings ─────────────────────────────────────────────────────
@@ -176,7 +204,7 @@ export const useAdminStore = defineStore('admin', () => {
} }
async function createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot> { async function createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot> {
return post<TimeSlot>('/admin/time-slot/manual', dto) return post<TimeSlot>('/admin/time-slot/manual', dto as unknown as Record<string, unknown>)
} }
async function closeSlot(id: string): Promise<TimeSlot> { async function closeSlot(id: string): Promise<TimeSlot> {

View File

@@ -1,16 +1,7 @@
import type { ApiResponse, PaginatedData } from '@mp-pilates/shared' import type { ApiResponse, PaginatedData } from '@mp-pilates/shared'
const BASE_URL = (() => { // 统一使用线上服务地址
try { const BASE_URL = 'https://focus.richarjiang.com/api'
const { miniProgram } = uni.getAccountInfoSync()
if (miniProgram.envVersion !== 'develop') {
return 'https://focus.richarjiang.com/api'
}
} catch {
// 非小程序环境,使用开发地址
}
return 'http://localhost:3000/api'
})()
interface RequestOptions { interface RequestOptions {
readonly url: string readonly url: string