mirror of
https://github.com/34892002/edgeKey.git
synced 2026-09-03 06:47:41 +08:00
feat: 实体商品
This commit is contained in:
@@ -75,6 +75,32 @@ bunx prisma migrate diff \
|
||||
--script > prisma/migrations/000X_描述.sql
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **必须手动核查生成的 SQL**,不要直接使用 `prisma migrate diff` 的输出。常见问题:
|
||||
>
|
||||
> **1. 枚举变更加新列 —— 实际不需要任何 DDL**
|
||||
>
|
||||
> Prisma 的 `enum` 在 SQLite 中就是普通 `TEXT` 列,数据库层面没有约束。因此:
|
||||
> - 在 `schema.prisma` 的 enum 中新增值(如 `ProductDeliveryType` 加 `EXPRESS`),**不需要任何 SQL 迁移**,因为 SQLite 不关心列里存的文本内容。
|
||||
> - 只有当 enum 对应的列首次创建时,才需要 `CREATE TABLE` / `ALTER TABLE ADD COLUMN`。
|
||||
>
|
||||
> **2. 加列操作不要用全表重建**
|
||||
>
|
||||
> `prisma migrate diff` 对 SQLite 经常生成 `CREATE _new_Table → INSERT → DROP TABLE → RENAME` 的全量重建模式。对于只是新增可空列的场景,应该替换为简单的:
|
||||
> ```sql
|
||||
> ALTER TABLE "TableName" ADD COLUMN "columnName" TEXT;
|
||||
> ```
|
||||
>
|
||||
> **3. 正确的决策流程**
|
||||
>
|
||||
> | schema 变更类型 | 迁移 SQL |
|
||||
> | --- |
|
||||
> | enum 新增值 | 无 SQL(Prisma 层面约束,SQLite 无需 DDL) |
|
||||
> | 新增可空列 | `ALTER TABLE ... ADD COLUMN ...` |
|
||||
> | 新增非空列(有默认值) | `ALTER TABLE ... ADD COLUMN ... DEFAULT ...` |
|
||||
> | 新增表 | `CREATE TABLE ...` |
|
||||
> | 删除列 / 改列类型 | SQLite 不支持,需全表重建(此时才用 CREATE _new → INSERT → DROP → RENAME 模式) |
|
||||
|
||||
#### 步骤2: 同步到本地开发环境
|
||||
|
||||
```bash
|
||||
@@ -306,7 +332,7 @@ try {
|
||||
1. **不要**假设 `bun dev` 使用的是 `prisma/db.sqlite`;当前它实际使用的是本地 D1 模拟器
|
||||
2. **不要**使用 `prisma migrate dev`,这会偏离当前 D1 迁移工作流
|
||||
3. **不要**反复覆盖 `prisma/migrations/0001_init.sql`;初始化迁移和后续增量迁移应分开维护
|
||||
4. **不要**信任 Prisma 生成的迁移 SQL,必须手动核查脚本,重点识别并拦截非预期的 **DROP TABLE** 或**全量重建**逻辑
|
||||
4. **不要**信任 Prisma 生成的迁移 SQL,必须手动核查。重点规则见上方「步骤1」中的决策流程表:enum 变更无需 DDL、新增列用 `ALTER TABLE`、只有删列/改类型才需要全表重建
|
||||
5. **不要**使用`node:fs`、`node:path`等Node.js原生模块
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ export function validateProductInput(input: {
|
||||
}
|
||||
|
||||
const deliveryType = input.deliveryType || "CARD_AUTO";
|
||||
if (!["CARD_AUTO", "FIXED_CARD", "MANUAL"].includes(deliveryType)) {
|
||||
if (!["CARD_AUTO", "FIXED_CARD", "MANUAL", "EXPRESS"].includes(deliveryType)) {
|
||||
throw badRequestError("发货方式不正确", "PRODUCT_DELIVERY_TYPE_INVALID");
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function validateProductInput(input: {
|
||||
|
||||
return {
|
||||
name,
|
||||
deliveryType: deliveryType as "CARD_AUTO" | "FIXED_CARD" | "MANUAL",
|
||||
deliveryType: deliveryType as "CARD_AUTO" | "FIXED_CARD" | "MANUAL" | "EXPRESS",
|
||||
fixedDeliveryContent,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export function upsertProductRecord(
|
||||
description?: string | null;
|
||||
price: number;
|
||||
status: "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||
deliveryType: "CARD_AUTO" | "FIXED_CARD" | "MANUAL";
|
||||
deliveryType: "CARD_AUTO" | "FIXED_CARD" | "MANUAL" | "EXPRESS";
|
||||
fixedDeliveryContent?: string | null;
|
||||
manualDeliveryHint?: string | null;
|
||||
stockMode: "FINITE" | "UNLIMITED";
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function saveProduct(input: {
|
||||
description?: string;
|
||||
price: number;
|
||||
status: "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||
deliveryType?: "CARD_AUTO" | "FIXED_CARD" | "MANUAL";
|
||||
deliveryType?: "CARD_AUTO" | "FIXED_CARD" | "MANUAL" | "EXPRESS";
|
||||
fixedDeliveryContent?: string;
|
||||
manualDeliveryHint?: string;
|
||||
minBuy: number;
|
||||
@@ -261,7 +261,7 @@ export async function saveProduct(input: {
|
||||
status: input.status,
|
||||
deliveryType,
|
||||
fixedDeliveryContent: deliveryType === "FIXED_CARD" ? fixedDeliveryContent : null,
|
||||
manualDeliveryHint: deliveryType === "MANUAL" ? input.manualDeliveryHint?.trim() || null : null,
|
||||
manualDeliveryHint: (deliveryType === "MANUAL" || deliveryType === "EXPRESS") ? input.manualDeliveryHint?.trim() || null : null,
|
||||
stockMode,
|
||||
minBuy,
|
||||
maxBuy,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ProductDeliveryTypeValue = "CARD_AUTO" | "FIXED_CARD" | "MANUAL";
|
||||
export type ProductDeliveryTypeValue = "CARD_AUTO" | "FIXED_CARD" | "MANUAL" | "EXPRESS";
|
||||
|
||||
export interface ProductSummary {
|
||||
id: number;
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function deliverOrder(prisma: PrismaClient, orderNo: string) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (order.product.deliveryType === "MANUAL") {
|
||||
if (order.product.deliveryType === "MANUAL" || order.product.deliveryType === "EXPRESS") {
|
||||
logger.info("manual_delivery_waiting", {
|
||||
event: "delivery.manual.waiting",
|
||||
orderNo: order.orderNo,
|
||||
@@ -158,7 +158,7 @@ export async function adminDeliverOrder(prisma: PrismaClient, orderId: number, i
|
||||
throw conflictError("订单已发货,无需重复发货", "ORDER_ALREADY_DELIVERED");
|
||||
}
|
||||
|
||||
if (order.product.deliveryType !== "MANUAL") {
|
||||
if (order.product.deliveryType !== "MANUAL" && order.product.deliveryType !== "EXPRESS") {
|
||||
return deliverOrder(prisma, order.orderNo);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export async function adminDeliverOrder(prisma: PrismaClient, orderId: number, i
|
||||
await prisma.orderDelivery.create({
|
||||
data: {
|
||||
orderId: order.id,
|
||||
deliveryType: "MANUAL",
|
||||
deliveryType: order.product.deliveryType === "EXPRESS" ? "EXPRESS" : "MANUAL",
|
||||
contentSnapshot: content,
|
||||
status: "SUCCESS",
|
||||
},
|
||||
|
||||
@@ -55,6 +55,7 @@ export function createOrderRecord(
|
||||
contactType: "EMAIL" | "QQ" | "TELEGRAM" | "OTHER";
|
||||
contactValue?: string | null;
|
||||
buyerNote?: string | null;
|
||||
receiverInfo?: string | null;
|
||||
paymentProvider: PaymentProvider;
|
||||
paymentChannel?: string | null;
|
||||
discountCodeId?: number | null;
|
||||
@@ -75,6 +76,7 @@ export function createOrderRecord(
|
||||
contactType: input.contactType,
|
||||
contactValue: input.contactValue ?? null,
|
||||
buyerNote: input.buyerNote ?? null,
|
||||
receiverInfo: input.receiverInfo ?? null,
|
||||
paymentProvider: input.paymentProvider,
|
||||
paymentChannel: input.paymentChannel ?? null,
|
||||
discountCodeId: input.discountCodeId ?? null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getContext } from "telefunc";
|
||||
import type { PaymentProvider } from "../payment/types";
|
||||
import type { PrismaClient } from "../../generated/prisma/client";
|
||||
import { conflictError, notFoundError } from "../../lib/app-error";
|
||||
import { badRequestError, conflictError, notFoundError } from "../../lib/app-error";
|
||||
import { validateOrderInput } from "../../lib/validators/order";
|
||||
import { getAdminContext, logAdminOperation } from "../auth/service";
|
||||
import { getAdminProductById } from "../catalog/service";
|
||||
@@ -47,6 +47,7 @@ export async function createOrder(input: {
|
||||
contactType: "EMAIL" | "QQ" | "TELEGRAM" | "OTHER";
|
||||
contactValue?: string;
|
||||
buyerNote?: string;
|
||||
receiverInfo?: string;
|
||||
discountCode?: string;
|
||||
}) {
|
||||
const { prisma } = getOrderContext();
|
||||
@@ -76,6 +77,10 @@ export async function createOrder(input: {
|
||||
throw conflictError("商品固定发货内容未配置,暂不可购买", "PRODUCT_FIXED_CONTENT_MISSING");
|
||||
}
|
||||
|
||||
if (product.deliveryType === "EXPRESS" && !input.receiverInfo?.trim()) {
|
||||
throw badRequestError("收货信息不能为空", "RECEIVER_INFO_REQUIRED");
|
||||
}
|
||||
|
||||
const orderNo = generateOrderNo();
|
||||
const queryToken = generateQueryToken();
|
||||
let paymentChannel: string | null = null;
|
||||
@@ -112,6 +117,7 @@ export async function createOrder(input: {
|
||||
contactType: input.contactType,
|
||||
contactValue,
|
||||
buyerNote: input.buyerNote?.trim() || null,
|
||||
receiverInfo: input.receiverInfo?.trim() || null,
|
||||
paymentProvider: input.paymentProvider,
|
||||
paymentChannel,
|
||||
discountCodeId,
|
||||
@@ -184,6 +190,7 @@ export async function createOrder(input: {
|
||||
contactType: input.contactType,
|
||||
contactValue,
|
||||
buyerNote: input.buyerNote?.trim() || null,
|
||||
receiverInfo: input.receiverInfo?.trim() || null,
|
||||
paymentProvider: input.paymentProvider,
|
||||
paymentChannel,
|
||||
discountCodeId,
|
||||
@@ -255,7 +262,7 @@ export async function getOrderForQuery(
|
||||
// notify 与 return 几乎同时到达时,return 这次读取可能正好卡在
|
||||
// “订单已支付但异步发货还没写完”的瞬间。这里做一次短暂重查,
|
||||
// 优先把最终的 DELIVERED 状态和发货内容返回给页面,避免用户手动刷新。
|
||||
if (order.product.deliveryType !== "MANUAL" && order.paymentStatus === "PAID" && order.deliveryStatus === "NOT_DELIVERED") {
|
||||
if (order.product.deliveryType !== "MANUAL" && order.product.deliveryType !== "EXPRESS" && order.paymentStatus === "PAID" && order.deliveryStatus === "NOT_DELIVERED") {
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
await sleep(150);
|
||||
const refreshed = await findOrderWithProduct(client, orderNo);
|
||||
@@ -457,6 +464,7 @@ export async function getAdminOrderById(id: number, prisma?: PrismaClient) {
|
||||
deliveryStatus: order.deliveryStatus,
|
||||
contactValue: order.contactValue,
|
||||
buyerNote: order.buyerNote,
|
||||
receiverInfo: order.receiverInfo,
|
||||
createdAt: order.createdAt.toISOString(),
|
||||
paidAt: order.paidAt ? order.paidAt.toISOString() : null,
|
||||
deliveredAt: order.deliveredAt ? order.deliveredAt.toISOString() : null,
|
||||
|
||||
@@ -593,7 +593,7 @@ export async function handlePaymentNotify(
|
||||
select: { deliveryType: true },
|
||||
});
|
||||
|
||||
if (product?.deliveryType !== "MANUAL") {
|
||||
if (product?.deliveryType !== "MANUAL" && product?.deliveryType !== "EXPRESS") {
|
||||
try {
|
||||
await deliverOrder(prisma, order.orderNo);
|
||||
message = "already paid; delivery retried";
|
||||
|
||||
@@ -26,20 +26,21 @@
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div>联系方式:{{ order.contactValue || '-' }}</div>
|
||||
<div v-if="order.receiverInfo">收货信息:{{ order.receiverInfo }}</div>
|
||||
<div>备注:{{ order.buyerNote || '-' }}</div>
|
||||
<div>创建时间:{{ formatDate(order.createdAt) }}</div>
|
||||
<div>查询凭证:<code>{{ order.queryToken }}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="order.productDeliveryType === 'MANUAL' && order.paymentStatus === 'PAID' && order.deliveryStatus !== 'DELIVERED'" class="pt-2">
|
||||
<div v-if="(order.productDeliveryType === 'MANUAL' || order.productDeliveryType === 'EXPRESS') && order.paymentStatus === 'PAID' && order.deliveryStatus !== 'DELIVERED'" class="pt-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">手动发货内容</span>
|
||||
<textarea v-model="manualDeliveryContent" class="textarea textarea-bordered w-full" rows="4" placeholder="填写本次订单要发给买家的内容"></textarea>
|
||||
<span class="label-text font-medium">{{ order.productDeliveryType === 'EXPRESS' ? '快递发货内容' : '手动发货内容' }}</span>
|
||||
<textarea v-model="manualDeliveryContent" class="textarea textarea-bordered w-full" rows="4" :placeholder="order.productDeliveryType === 'EXPRESS' ? '例如:快递单号、快递公司等物流信息' : '例如:账号密码、激活码等买家需要的内容'"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3 pt-2">
|
||||
<AppButton size="sm" variant="primary" :disabled="order.deliveryStatus === 'DELIVERED' || order.paymentStatus !== 'PAID'" @click="handleRedeliver">{{ deliveryActionLabel }}</AppButton>
|
||||
<AppButton size="sm" variant="outline" :disabled="order.status === 'CLOSED'" @click="handleClose">关闭订单</AppButton>
|
||||
<AppButton size="sm" variant="primary" :loading="delivering" :disabled="order.deliveryStatus === 'DELIVERED' || order.paymentStatus !== 'PAID'" @click="handleRedeliver">{{ deliveryActionLabel }}</AppButton>
|
||||
<AppButton size="sm" variant="outline" :loading="closing" :disabled="order.status === 'CLOSED'" @click="handleClose">关闭订单</AppButton>
|
||||
<span v-if="actionMessage" class="text-sm text-success">{{ actionMessage }}</span>
|
||||
<span v-if="actionError" class="text-sm text-error">{{ actionError }}</span>
|
||||
</div>
|
||||
@@ -80,7 +81,8 @@
|
||||
<pre class="bg-base-200 rounded-box p-3 text-xs overflow-x-auto whitespace-pre-wrap break-all">{{ formattedPayload }}</pre>
|
||||
<div class="modal-action">
|
||||
<form method="dialog"><button class="btn btn-sm">关闭</button></form>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop"><button>关闭</button></form>
|
||||
</dialog>
|
||||
</div>
|
||||
@@ -112,6 +114,8 @@ import type { Data } from "./+data";
|
||||
const { order } = useData<Data>();
|
||||
const actionMessage = ref("");
|
||||
const actionError = ref("");
|
||||
const delivering = ref(false);
|
||||
const closing = ref(false);
|
||||
const manualDeliveryContent = ref("");
|
||||
const payloadDialogRef = ref<HTMLDialogElement | null>(null);
|
||||
const formattedPayload = ref("");
|
||||
@@ -119,6 +123,7 @@ const formattedPayload = ref("");
|
||||
const deliveryActionLabel = computed(() => {
|
||||
if (!order) return "发货";
|
||||
if (order.productDeliveryType === "MANUAL") return "手动发货";
|
||||
if (order.productDeliveryType === "EXPRESS") return "快递发货";
|
||||
if (order.productDeliveryType === "FIXED_CARD") return "发货固定内容";
|
||||
return "重新自动发货";
|
||||
});
|
||||
@@ -137,28 +142,36 @@ function formatDate(value: string) {
|
||||
}
|
||||
|
||||
async function handleRedeliver() {
|
||||
if (!order) return;
|
||||
if (!order || delivering.value) return;
|
||||
actionMessage.value = "";
|
||||
actionError.value = "";
|
||||
delivering.value = true;
|
||||
|
||||
try {
|
||||
const result = await onRedeliver({ orderId: order.id, content: manualDeliveryContent.value });
|
||||
order.deliveryStatus = "DELIVERED";
|
||||
actionMessage.value = `${deliveryActionLabel.value}完成,共发出 ${result.items.length} 条内容。`;
|
||||
} catch (error) {
|
||||
actionError.value = normalizeTelefuncError(error, "发货失败");
|
||||
} finally {
|
||||
delivering.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClose() {
|
||||
if (!order) return;
|
||||
if (!order || closing.value) return;
|
||||
actionMessage.value = "";
|
||||
actionError.value = "";
|
||||
closing.value = true;
|
||||
|
||||
try {
|
||||
await onCloseOrder({ orderId: order.id });
|
||||
actionMessage.value = "订单已关闭,请刷新查看最新状态。";
|
||||
order.status = "CLOSED";
|
||||
actionMessage.value = "订单已关闭。";
|
||||
} catch (error) {
|
||||
actionError.value = normalizeTelefuncError(error, "关闭失败");
|
||||
} finally {
|
||||
closing.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,6 +98,8 @@ function getDeliveryTypeLabel(deliveryType: string) {
|
||||
return "固定内容自动发货";
|
||||
case "MANUAL":
|
||||
return "手动发货";
|
||||
case "EXPRESS":
|
||||
return "快递发货";
|
||||
default:
|
||||
return deliveryType;
|
||||
}
|
||||
@@ -111,6 +113,8 @@ function getDeliveryTypeTagType(deliveryType: string): "primary" | "success" | "
|
||||
return "success";
|
||||
case "MANUAL":
|
||||
return "warning";
|
||||
case "EXPRESS":
|
||||
return "warning";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="text-sm font-medium">发货方式</div>
|
||||
<p class="text-xs text-base-content/60">发货方式决定库存来源和支付后的发货动作。</p>
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="grid gap-3 md:grid-cols-4">
|
||||
<label class="rounded-box border border-base-300 p-3" :class="form.deliveryType === 'CARD_AUTO' ? 'border-primary bg-primary/5' : ''">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium">自动发货卡密</span>
|
||||
@@ -68,6 +68,13 @@
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-base-content/60">支付后等待管理员在订单详情填写发货内容。</p>
|
||||
</label>
|
||||
<label class="rounded-box border border-base-300 p-3" :class="form.deliveryType === 'EXPRESS' ? 'border-primary bg-primary/5' : ''">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-medium">快递发货</span>
|
||||
<input v-model="form.deliveryType" type="radio" class="radio radio-primary radio-sm" value="EXPRESS" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-base-content/60">买家下单时填写收货信息,支付后管理员安排快递发货。</p>
|
||||
</label>
|
||||
</div>
|
||||
<label v-if="form.deliveryType === 'FIXED_CARD'" class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">固定发货内容</span>
|
||||
@@ -77,6 +84,10 @@
|
||||
<span class="label-text font-medium">手动发货说明(可选)</span>
|
||||
<textarea v-model="form.manualDeliveryHint" class="textarea textarea-bordered w-full" rows="3" placeholder="例如:请留下账号信息,管理员将在 24 小时内处理"></textarea>
|
||||
</label>
|
||||
<label v-if="form.deliveryType === 'EXPRESS'" class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">快递发货说明(可选)</span>
|
||||
<textarea v-model="form.manualDeliveryHint" class="textarea textarea-bordered w-full" rows="3" placeholder="例如:请填写收货地址,管理员将在 48 小时内安排发货"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface ProductFormState {
|
||||
description: string;
|
||||
price: number;
|
||||
status: "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||
deliveryType: "CARD_AUTO" | "FIXED_CARD" | "MANUAL";
|
||||
deliveryType: "CARD_AUTO" | "FIXED_CARD" | "MANUAL" | "EXPRESS";
|
||||
fixedDeliveryContent: string;
|
||||
manualDeliveryHint: string;
|
||||
minBuy: number;
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function onSaveProduct(input: {
|
||||
description?: string;
|
||||
price: number;
|
||||
status: "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||
deliveryType?: "CARD_AUTO" | "FIXED_CARD" | "MANUAL";
|
||||
deliveryType?: "CARD_AUTO" | "FIXED_CARD" | "MANUAL" | "EXPRESS";
|
||||
fixedDeliveryContent?: string;
|
||||
manualDeliveryHint?: string;
|
||||
minBuy: number;
|
||||
|
||||
@@ -122,6 +122,12 @@
|
||||
>
|
||||
人工发货
|
||||
</span>
|
||||
<span
|
||||
v-else-if="product.deliveryType === 'EXPRESS'"
|
||||
class="text-xs font-semibold px-2 py-0.5 rounded bg-violet-500/10 text-violet-600"
|
||||
>
|
||||
实体商品
|
||||
</span>
|
||||
<div class="flex items-baseline gap-0.5">
|
||||
<span class="text-[11px] font-bold text-red-500/60">¥</span>
|
||||
<span class="text-xl font-bold text-red-500 leading-none">{{ formatCents(product.price) }}</span>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
:disabled="discountPreview.loading"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn btn-outline"
|
||||
:disabled="!form.discountCode.trim() || discountPreview.loading"
|
||||
@click="handlePreviewDiscount"
|
||||
>
|
||||
@@ -65,6 +65,12 @@
|
||||
<p v-if="discountPreview.error" class="-mt-2 text-xs text-error">{{ discountPreview.error }}</p>
|
||||
<p v-if="discountPreview.valid" class="-mt-2 text-xs text-orange-400">折扣码有效,优惠 {{ formatCents(discountPreview.discount) }}</p>
|
||||
|
||||
<label v-if="product.deliveryType === 'EXPRESS'" class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">收货信息 <span class="text-error">*</span></span>
|
||||
<textarea v-model="form.receiverInfo" class="textarea textarea-bordered w-full" rows="3" placeholder="请填写收货信息,例如:
|
||||
张三,13812341234,广东省深圳市xxx"></textarea>
|
||||
</label>
|
||||
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">备注</span>
|
||||
<textarea v-model="form.buyerNote" class="textarea textarea-bordered w-full" rows="3" placeholder="可以留下QQ号、微信等联系方式"></textarea>
|
||||
@@ -128,6 +134,7 @@
|
||||
</p>
|
||||
<p v-else-if="product.deliveryType === 'FIXED_CARD'" class="text-sm text-success">自动发货,库存充足。</p>
|
||||
<p v-else-if="product.deliveryType === 'MANUAL'" class="text-sm text-success">{{ product.manualDeliveryHint || '支付后,客服将尽快为您处理订单,请耐心等待。' }}</p>
|
||||
<p v-else-if="product.deliveryType === 'EXPRESS'" class="text-sm text-success">{{ product.manualDeliveryHint || '请填写收货信息,支付后管理员将安排快递发货。' }}</p>
|
||||
|
||||
<AppButton variant="primary" :loading="submitting" :disabled="(!isFreeOrder && !paymentMethods.length) || (product.deliveryType === 'CARD_AUTO' && product.availableStock === 0)" @click="handleCreateOrder">
|
||||
{{ product.deliveryType === 'CARD_AUTO' && product.availableStock === 0 ? '已售罄' : isFreeOrder ? '免费获取' : '提交订单' }}
|
||||
@@ -177,13 +184,14 @@ const form = reactive({
|
||||
quantity: product?.minBuy ?? 1,
|
||||
contactValue: "",
|
||||
buyerNote: "",
|
||||
receiverInfo: "",
|
||||
discountCode: "",
|
||||
paymentProvider: paymentMethods[0]?.provider ?? "",
|
||||
paymentChannel: getDefaultPaymentChannel(paymentMethods[0]?.provider ?? ""),
|
||||
});
|
||||
|
||||
function getDeliveryTypeLabel(type: string) {
|
||||
return ({ CARD_AUTO: "自动发货", FIXED_CARD: "自动发货", MANUAL: "人工发货" } as Record<string, string>)[type] || type;
|
||||
return ({ CARD_AUTO: "自动发货", FIXED_CARD: "自动发货", MANUAL: "人工发货", EXPRESS: "快递发货" } as Record<string, string>)[type] || type;
|
||||
}
|
||||
|
||||
let mobile = false;
|
||||
@@ -260,6 +268,11 @@ async function handleCreateOrder() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (product.deliveryType === 'EXPRESS' && !form.receiverInfo.trim()) {
|
||||
errorMessage.value = "收货信息不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
errorMessage.value = "";
|
||||
|
||||
@@ -275,6 +288,7 @@ async function handleCreateOrder() {
|
||||
contactType: "EMAIL",
|
||||
contactValue: contactEmail,
|
||||
buyerNote: form.buyerNote,
|
||||
receiverInfo: form.receiverInfo,
|
||||
discountCode: form.discountCode.trim() || undefined,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function onCreateOrder(input: {
|
||||
contactType: "EMAIL";
|
||||
contactValue: string;
|
||||
buyerNote?: string;
|
||||
receiverInfo?: string;
|
||||
discountCode?: string;
|
||||
}) {
|
||||
try {
|
||||
|
||||
2
prisma/migrations/0007_add_express_delivery.sql
Normal file
2
prisma/migrations/0007_add_express_delivery.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable: Add receiver info field for express delivery
|
||||
ALTER TABLE "Order" ADD COLUMN "receiverInfo" TEXT;
|
||||
@@ -28,6 +28,7 @@ enum ProductDeliveryType {
|
||||
CARD_AUTO
|
||||
FIXED_CARD
|
||||
MANUAL
|
||||
EXPRESS
|
||||
}
|
||||
|
||||
enum ProductStockMode {
|
||||
@@ -93,6 +94,7 @@ enum DeliveryRecordType {
|
||||
CARD
|
||||
FIXED_CARD
|
||||
MANUAL
|
||||
EXPRESS
|
||||
}
|
||||
|
||||
enum DeliveryRecordStatus {
|
||||
@@ -233,6 +235,7 @@ model Order {
|
||||
contactType ContactType @default(EMAIL)
|
||||
contactValue String?
|
||||
buyerNote String?
|
||||
receiverInfo String?
|
||||
paymentProvider String
|
||||
paymentChannel String?
|
||||
paymentOrderNo String?
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"binding": "DB",
|
||||
"database_name": "edgekey-db",
|
||||
"migrations_dir": "prisma/migrations",
|
||||
// "database_id": "24390dbc-b9c6-4ae6-8c7f-507fb2eb36f2", // 执行 wrangler d1 命令必须,但是与cf一键部署冲突所以注释
|
||||
"database_id": "24390dbc-b9c6-4ae6-8c7f-507fb2eb36f2", // 执行 wrangler d1 命令必须,但是与cf一键部署冲突所以注释
|
||||
}
|
||||
],
|
||||
"triggers": {
|
||||
|
||||
Reference in New Issue
Block a user