From a7399e062495a684af8c66b84079da890357bca4 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 17 Sep 2026 03:39:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(tender):=20=E6=88=91=E7=9A=84=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E5=8A=A0=E3=80=8C=E5=8E=BB=E6=94=AF=E4=BB=98=E3=80=8D?= =?UTF-8?q?=E3=80=8C=E5=8F=96=E6=B6=88=E8=AE=A2=E5=8D=95=E3=80=8D=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E5=B9=B6=E6=96=B0=E5=BB=BA=E6=94=B6=E9=93=B6?= =?UTF-8?q?=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hjc-web 此前**没有收银台**——支付步骤内嵌在购标流程(BuyDocument 的 step='pay')里, 买家一旦离开就再也回不到未完成的支付。本提交补上按订单号付款的独立页面。 - 新建 /tender/pay 收银台:只认订单号,金额取自**订单本身**(by-no)而非支付响应 (支付一失败页面就会停在 0.00);进入先判状态,已支付/已取消/已退款**不发**统一下单, 就地显示结论——既无意义,也会多撞一次跨端 OUT_TRADE_NO_USED - 订单列表:待支付卡片加两个按钮 + 手搓 Tailwind 二次确认弹窗(照 ConsultDialog 的风格, 不用 antd 的 Modal:买家页无先例,视觉与站点自建风格不一致) - 状态口径改读 orderStatus(原先只看 payStatus,取消后会把已取消显示成「待支付」 并带上「去支付」),抽 app/utils/orderStatus.ts,与 hjc-h5 刻意保持同一套优先级 - 新增两个 Nitro 代理 order-by-no.get.ts / cancel.post.ts,照既有 my-orders / pay-status 的范式原样透传 ApiResult - 标书详情「购买人数」改读 buyerCount(后端实时统计的已付款订单数) - 跨端 OUT_TRADE_NO_USED:识别后端回带的 data.wechatCode 并给可行动提示, 不匹配 message 字符串(包装方式一改就会静默失效) --- app/components/HjcConfirmDialog.vue | 128 +++++++++ app/pages/tender/orders.vue | 160 ++++++++--- app/pages/tender/pay.vue | 262 ++++++++++++++++++ .../template-07/pages/TenderDetail.vue | 4 +- app/types/tender.ts | 7 +- app/utils/orderStatus.ts | 69 +++++ server/api/tender/cancel.post.ts | 44 +++ server/api/tender/order-by-no.get.ts | 41 +++ 8 files changed, 681 insertions(+), 34 deletions(-) create mode 100644 app/components/HjcConfirmDialog.vue create mode 100644 app/pages/tender/pay.vue create mode 100644 app/utils/orderStatus.ts create mode 100644 server/api/tender/cancel.post.ts create mode 100644 server/api/tender/order-by-no.get.ts diff --git a/app/components/HjcConfirmDialog.vue b/app/components/HjcConfirmDialog.vue new file mode 100644 index 0000000..ec5312f --- /dev/null +++ b/app/components/HjcConfirmDialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/app/pages/tender/orders.vue b/app/pages/tender/orders.vue index 555d176..493deea 100644 --- a/app/pages/tender/orders.vue +++ b/app/pages/tender/orders.vue @@ -12,28 +12,67 @@
{{ error }}
-
-
-
-
-

{{ o.projectName }}

-

订单号:{{ o.orderNo }} · 项目编号:{{ o.projectNo }}

+ + +
diff --git a/app/templates/template-07/pages/TenderDetail.vue b/app/templates/template-07/pages/TenderDetail.vue index 40a3e14..d58e65b 100644 --- a/app/templates/template-07/pages/TenderDetail.vue +++ b/app/templates/template-07/pages/TenderDetail.vue @@ -54,7 +54,9 @@

购买成功后可在「我的订单」查看并下载标书

-

购买人数:{{ tender.saleCount || 0 }}

+ +

购买人数:{{ tender.buyerCount || 0 }}

售卖方式:{{ sellingMethodText }}

diff --git a/app/types/tender.ts b/app/types/tender.ts index 087b837..fd90685 100644 --- a/app/types/tender.ts +++ b/app/types/tender.ts @@ -39,8 +39,13 @@ export interface Tender { status?: number /** 来源 */ dataSource?: string - /** 已售数量 */ + /** 已售数量(下单即加、从不回退;当前已无展示用途,历史字段) */ saleCount?: number + /** + * 购买人数:后端实时统计的**已付款订单数**(`COUNT(*) WHERE pay_status=1`)。 + * 口径与「人数」字面有偏差(同一企业买 2 单计 2),这是明确拍定的取值。 + */ + buyerCount?: number } /** 分页返回结构 */ diff --git a/app/utils/orderStatus.ts b/app/utils/orderStatus.ts new file mode 100644 index 0000000..079a7f2 --- /dev/null +++ b/app/utils/orderStatus.ts @@ -0,0 +1,69 @@ +/** + * 订单状态展示口径(hjc-web)。 + * + * **必须与 `hjc-h5/src/utils/orderStatus.js` 保持一致**——两端是不同仓库、不同技术栈 + * (Nuxt4 + TS / uni-app + JS),代码无法真正共享,只能靠"两处同一套定义"来对齐。 + * 改这里的时候,另一边也要改。 + * + * 优先级(从高到低): + * payStatus === 3 → 已退款 + * orderStatus === 2 → 已取消 + * payStatus === 1 || orderStatus === 1 → 已完成 + * 其余 → 待支付 + * + * 为什么需要它:hjc-web 此前**完全不读 `orderStatus`**(只看 payStatus), + * 于是订单一旦被置为已取消,卡片仍会显示「待支付」并带上「去支付」按钮。 + */ + +/** 订单状态归类键。四端(web / h5 / 小程序 / 后台)未来的展示都应落在这四个之一。 */ +export type HjcOrderStatusKey = 'pending' | 'done' | 'cancel' | 'refund' + +/** 判定所需的订单字段(只用到这两个,避免依赖完整的 HjcOrder 类型) */ +export interface HjcOrderLike { + payStatus?: number | null + orderStatus?: number | null +} + +export function hjcOrderStatusKey(o?: HjcOrderLike | null): HjcOrderStatusKey { + if (!o) return 'pending' + if (o.payStatus === 3) return 'refund' + if (o.orderStatus === 2) return 'cancel' + if (o.payStatus === 1 || o.orderStatus === 1) return 'done' + return 'pending' +} + +const LABELS: Record = { + pending: '待支付', + done: '已完成', + cancel: '已取消', + refund: '已退款' +} + +export function hjcOrderStatusLabel(o?: HjcOrderLike | null): string { + return LABELS[hjcOrderStatusKey(o)] +} + +/** 状态徽标的 Tailwind 配色 */ +export function hjcOrderStatusClass(o?: HjcOrderLike | null): string { + switch (hjcOrderStatusKey(o)) { + case 'done': + return 'bg-green-100 text-green-700' + case 'cancel': + case 'refund': + return 'bg-gray-100 text-gray-500' + default: + return 'bg-amber-100 text-amber-700' + } +} + +/** + * 这张订单是否该出现「去支付」「取消订单」。 + * + * **不要用 `hjcOrderStatusKey(o) === 'pending'` 当条件**:`payStatus=2`(支付失败)也会落进 + * `pending` 这个兜底键。虽然它目前是死状态(全仓 `setPayStatus` 只有 0/1/3 三个写入点), + * 但把"能不能操作"绑在一个展示口径的兜底分支上,是下次改动最容易踩的坑。 + */ +export function hjcCanActOnOrder(o?: HjcOrderLike | null): boolean { + if (!o) return false + return o.payStatus === 0 && o.orderStatus !== 2 +} diff --git a/server/api/tender/cancel.post.ts b/server/api/tender/cancel.post.ts new file mode 100644 index 0000000..857aad2 --- /dev/null +++ b/server/api/tender/cancel.post.ts @@ -0,0 +1,44 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../utils/tenant' + +/** + * 取消订单(买家取消自己的待支付订单,幂等) + * POST /api/tender/cancel { orderNo } + * 代理到 mp-api /api/hjc/order/cancel;需登录态。 + * + * 约定:原样透传 ApiResult{code,message,data},由前端按 body.code 判定登录态与业务失败。 + * + * 注意后端的返回语义:**「其实已经付了」不是错误**,它同样返回 code=0, + * 结论放在 data 的结构化字段里 —— `{ orderNo, cancelled, paid, verified, payStatus, orderStatus }`。 + * 前端必须按字段判定,**不要**去匹配 message 字符串。 + * - cancelled=true → 取消成功 + * - cancelled=false, paid=true → 该订单已支付(后端已顺手把本地状态补齐),刷新列表即可看到「已完成」 + * - verified=false → 本次未能在微信侧核对(微信不可达),取消仍然生效 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const body = await readBody(event) + const cookieToken = getCookie(event, 'hjc_token') + const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null) + + try { + return await $fetch('/hjc/order/cancel', { + baseURL: config.public.modulesApiBase, + method: 'POST', + headers: { + TenantId: ctx.tenantId, + ...(auth ? { Authorization: auth } : {}) + }, + query: { TenantId: ctx.tenantId }, + body + }) + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.statusMessage || 'Failed to cancel order' + }) + } +}) diff --git a/server/api/tender/order-by-no.get.ts b/server/api/tender/order-by-no.get.ts new file mode 100644 index 0000000..65e7824 --- /dev/null +++ b/server/api/tender/order-by-no.get.ts @@ -0,0 +1,41 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, getHeader, getCookie, getQuery } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../utils/tenant' + +/** + * 按订单号取订单(收银台用:它手上只有订单号,靠这个拿应付金额与当前状态) + * GET /api/tender/order-by-no?orderNo=xxx + * 代理到 mp-api /api/hjc/order/by-no/{orderNo};需登录态。 + * + * 为什么金额不跟 URL 传:收银台可能被刷新、也可能在网页授权回跳后重新进入; + * 钱数从 URL 来意味着展示值可被改(扣款仍以服务端为准,但纯展示也不该给假数)。 + * 后端 `GET /api/hjc/order/by-no/{orderNo}` 的注释就写着「收银台只有订单号,用它取应付金额」。 + * + * 约定:原样透传 ApiResult{code,message,data}。HTTP 状态码恒为 200, + * 「未登录/token 失效」是 code=401、「无权限」是 code=403,前端必须按 code 判定。 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const { orderNo } = getQuery(event) + const cookieToken = getCookie(event, 'hjc_token') + const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null) + + try { + return await $fetch(`/hjc/order/by-no/${encodeURIComponent(String(orderNo ?? ''))}`, { + baseURL: config.public.modulesApiBase, + method: 'GET', + headers: { + TenantId: ctx.tenantId, + ...(auth ? { Authorization: auth } : {}) + }, + query: { TenantId: ctx.tenantId } + }) + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.statusMessage || 'Failed to fetch order by orderNo' + }) + } +})