diff --git a/app/composables/useHjcAuth.ts b/app/composables/useHjcAuth.ts new file mode 100644 index 0000000..cc9723f --- /dev/null +++ b/app/composables/useHjcAuth.ts @@ -0,0 +1,47 @@ +import type { HjcUser } from '~/types/tender' + +/** 登录态(token 存 cookie,SSR 安全;服务器端代理从 cookie 读取并转发 Authorization) */ +export function useHjcAuth() { + const token = useCookie('hjc_token', { + maxAge: 60 * 60 * 24 * 7, + sameSite: 'lax' + }) + const user = useState('hjc-user', () => null) + + function isLoggedIn() { + return !!token.value + } + + async function login(enterpriseName: string, password: string) { + const res: any = await $fetch('/api/tender/login', { + method: 'POST', + body: { enterpriseName, password } + }) + if (res?.access_token) { + token.value = res.access_token + user.value = res.user || null + return { ok: true, user: res.user } + } + return { ok: false, message: res?.message || '登录失败' } + } + + async function register(payload: Record) { + const res: any = await $fetch('/api/tender/register', { + method: 'POST', + body: payload + }) + if (res?.access_token) { + token.value = res.access_token + user.value = res.user || null + return { ok: true, user: res.user } + } + return { ok: false, message: res?.message || '注册失败' } + } + + function logout() { + token.value = null + user.value = null + } + + return { token, user, isLoggedIn, login, register, logout } +} diff --git a/app/pages/login.vue b/app/pages/login.vue new file mode 100644 index 0000000..2d47b59 --- /dev/null +++ b/app/pages/login.vue @@ -0,0 +1,50 @@ + + + diff --git a/app/pages/register.vue b/app/pages/register.vue new file mode 100644 index 0000000..6801b25 --- /dev/null +++ b/app/pages/register.vue @@ -0,0 +1,72 @@ + + + diff --git a/app/templates/template-07/pages/BuyDocument.vue b/app/templates/template-07/pages/BuyDocument.vue index d133aec..585ae45 100644 --- a/app/templates/template-07/pages/BuyDocument.vue +++ b/app/templates/template-07/pages/BuyDocument.vue @@ -2,7 +2,17 @@
← 返回标书中心 -
+ +
+

购买标书需要先登录企业账号

+
+ 去登录 + 企业注册 +
+
+ + +

购买标书

{{ tender.projectName }}

@@ -30,19 +40,34 @@
- - -

{{ tip }}

{{ error }}

+ +
+

请使用微信扫码支付

+

订单号:{{ orderNo }}

+

¥{{ formatMoney(totalAmount) }}

+
+ 扫码链接:{{ codeUrl }} +
+ +

{{ error }}

+
+ + +
+

购买成功

+

订单号:{{ orderNo }},标书已可查看/下载

+ 返回标书中心 +
+
项目不存在
@@ -53,18 +78,21 @@ import type { Tender } from '~/types/tender' const route = useRoute() const id = route.query.id as string +const { isLoggedIn } = useHjcAuth() + +const loggedIn = ref(false) const tender = shallowRef(null) const notFound = ref(false) const submitting = ref(false) -const tip = ref('') +const marking = ref(false) const error = ref('') +const step = ref<'form' | 'pay' | 'done'>('form') +const orderNo = ref('') +const codeUrl = ref('') +const totalAmount = ref(0) -const form = reactive({ - contactName: '', - contactPhone: '', - contactEmail: '', - quantity: 1 -}) +const form = reactive({ contactName: '', contactPhone: '', contactEmail: '', quantity: 1 }) +const redirect = computed(() => `/buy?id=${id}`) function formatMoney(n?: number | string) { return Number(n ?? 0).toFixed(2) @@ -72,10 +100,9 @@ function formatMoney(n?: number | string) { async function submit() { error.value = '' - tip.value = '' submitting.value = true try { - const res: any = await $fetch('/api/tender/order', { + const order: any = await $fetch('/api/tender/order', { method: 'POST', body: { projectId: Number(id), @@ -85,11 +112,15 @@ async function submit() { contactEmail: form.contactEmail } }) - if (res && res.orderNo) { - tip.value = `下单成功,订单号:${res.orderNo},请扫码支付` - // TODO(阶段3b): 调 /api/tender/pay 获取微信二维码(需登录态) + if (order && order.orderNo) { + orderNo.value = order.orderNo + totalAmount.value = order.totalAmount || 0 + // 发起支付 + const pay: any = await $fetch('/api/tender/pay', { method: 'POST', body: { orderNo: order.orderNo } }) + codeUrl.value = pay?.codeUrl || '' + step.value = 'pay' } else { - error.value = res?.message || '下单失败' + error.value = order?.message || '下单失败' } } catch (e: any) { error.value = e?.data?.message || e?.message || '下单失败' @@ -98,6 +129,21 @@ async function submit() { } } +async function markPaid() { + error.value = '' + marking.value = true + try { + await $fetch('/api/tender/mark-paid', { method: 'PUT', body: { orderNo: orderNo.value } }) + step.value = 'done' + } catch (e: any) { + error.value = e?.data?.message || e?.message || '确认失败' + } finally { + marking.value = false + } +} + +loggedIn.value = isLoggedIn() + try { if (!id) { notFound.value = true diff --git a/app/types/tender.ts b/app/types/tender.ts index 75b5183..087b837 100644 --- a/app/types/tender.ts +++ b/app/types/tender.ts @@ -48,3 +48,13 @@ export interface TenderPage { list: Tender[] count: number } + +/** 登录用户(对应后端 common.system.User 关键字段) */ +export interface HjcUser { + userId: number + username: string + nickname?: string + phone?: string + tenantId?: number + [key: string]: any +} diff --git a/server/api/tender/auth/login.post.ts b/server/api/tender/auth/login.post.ts new file mode 100644 index 0000000..e39df5f --- /dev/null +++ b/server/api/tender/auth/login.post.ts @@ -0,0 +1,32 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../../utils/tenant' + +/** + * 企业登录 + * POST /api/tender/login { enterpriseName, password } + * 代理到 mp-api /api/hjc/auth/login;成功返回 LoginResult{ access_token, user }。 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const body = await readBody(event) + + try { + const res = await $fetch('/hjc/auth/login', { + baseURL: config.public.modulesApiBase, + method: 'POST', + headers: { TenantId: ctx.tenantId }, + query: { TenantId: ctx.tenantId }, + body + }) as any + const data = res && res.data !== undefined ? res.data : res + return data + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.statusMessage || 'Failed to login' + }) + } +}) diff --git a/server/api/tender/auth/register.post.ts b/server/api/tender/auth/register.post.ts new file mode 100644 index 0000000..a12c2b3 --- /dev/null +++ b/server/api/tender/auth/register.post.ts @@ -0,0 +1,32 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../../utils/tenant' + +/** + * 企业注册 + * POST /api/tender/register { enterpriseName, password, creditCode, ... } + * 代理到 mp-api /api/hjc/auth/register;成功返回 LoginResult{ access_token, user }。 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const body = await readBody(event) + + try { + const res = await $fetch('/hjc/auth/register', { + baseURL: config.public.modulesApiBase, + method: 'POST', + headers: { TenantId: ctx.tenantId }, + query: { TenantId: ctx.tenantId }, + body + }) as any + const data = res && res.data !== undefined ? res.data : res + return data + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.statusMessage || 'Failed to register' + }) + } +}) diff --git a/server/api/tender/mark-paid.put.ts b/server/api/tender/mark-paid.put.ts new file mode 100644 index 0000000..24f5c19 --- /dev/null +++ b/server/api/tender/mark-paid.put.ts @@ -0,0 +1,37 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../utils/tenant' + +/** + * 标记订单已支付(幂等)并触发一站式推送 + * PUT /api/tender/mark-paid { orderNo } + * 代理到 mp-api /api/hjc/order/mark-paid;需登录态。 + */ +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 { + const res = await $fetch('/hjc/order/mark-paid', { + baseURL: config.public.modulesApiBase, + method: 'PUT', + headers: { + TenantId: ctx.tenantId, + ...(auth ? { Authorization: auth } : {}) + }, + query: { TenantId: ctx.tenantId }, + body + }) as any + const data = res && res.data !== undefined ? res.data : res + return data + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.statusMessage || 'Failed to mark paid' + }) + } +}) diff --git a/server/api/tender/order.post.ts b/server/api/tender/order.post.ts index b5a42cd..ee17649 100644 --- a/server/api/tender/order.post.ts +++ b/server/api/tender/order.post.ts @@ -1,18 +1,19 @@ import { $fetch } from 'ofetch' -import { createError, defineEventHandler, readBody, getHeader } from 'h3' +import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3' import { useRuntimeConfig } from '#imports' import { getTenantFromContext } from '../../utils/tenant' /** * 创建标书订单 * POST /api/tender/order { projectId, quantity, contactName, contactPhone, contactEmail } - * 代理到 mp-api(mp-java) /api/hjc/order/create;需登录态(Authorization 透传)。 + * 代理到 mp-api(mp-java) /api/hjc/order/create;需登录态(从 cookie hjc_token 或 Authorization 透传)。 */ export default defineEventHandler(async (event) => { const config = useRuntimeConfig() const ctx = getTenantFromContext(event, config) const body = await readBody(event) - const auth = getHeader(event, 'authorization') + const cookieToken = getCookie(event, 'hjc_token') + const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null) try { const res = await $fetch('/hjc/order/create', { diff --git a/server/api/tender/pay.post.ts b/server/api/tender/pay.post.ts new file mode 100644 index 0000000..e566af9 --- /dev/null +++ b/server/api/tender/pay.post.ts @@ -0,0 +1,37 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../utils/tenant' + +/** + * 发起支付(微信Native扫码,返回 codeUrl) + * POST /api/tender/pay { orderNo } + * 代理到 mp-api /api/hjc/order/pay;需登录态。 + */ +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 { + const res = await $fetch('/hjc/order/pay', { + baseURL: config.public.modulesApiBase, + method: 'POST', + headers: { + TenantId: ctx.tenantId, + ...(auth ? { Authorization: auth } : {}) + }, + query: { TenantId: ctx.tenantId }, + body + }) as any + const data = res && res.data !== undefined ? res.data : res + return data + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.statusMessage || 'Failed to pay' + }) + } +})