From 81ef9317c7c7942a845e3a146d354d17c4191d86 Mon Sep 17 00:00:00 2001 From: "weicw1996@qq.com" Date: Thu, 17 Sep 2026 02:35:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(tender):=20=E5=BF=98=E8=AE=B0=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E4=B8=8E=E4=BF=AE=E6=94=B9=E5=AF=86=E7=A0=81=E9=A1=B5?= =?UTF-8?q?=EF=BC=8810=20=E5=A5=97=E6=A8=A1=E6=9D=BF=E9=80=9A=E7=94=A8?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 密码找回(材料审核制,见 ADR 0008) - app/pages/forgot-password.vue:两个页签——「提交申请」(企业名称 + 纳税人识别号 + 新密码 + 确认 + 授权委托书,复用 POST /api/tender/upload,并照 HjcEnterpriseForm 做 5M 校验)与「查询进度」 (双要素查询,展示状态与时间、驳回原因)。提交后自动把条件带到查询页签。文案纪律: 「已通过」≠「已重置」。 修改密码(登录态,双因子) - app/pages/change-password.vue:旧密码 + 短信验证码(60s 倒计时,照 register.vue)+ 新密码 + 确认;成功后清本机凭据并显示「去登录」面板。 BFF 与封装 - server/api/tender/password/{apply.post,status.get,sms.post,change.put}.ts:四条都照 register.post.ts 的透传范式(modulesApiBase + TenantId(header 与 query 都带)+ 原样返回 ApiResult,HTTP 恒 200)。前两条**匿名**(与 sms/upload 一致,不读 cookie);后两条需登录态, 从 cookie hjc_token 或 Authorization 头取 token 拼 Bearer。 - app/composables/useHjcPassword.ts:四条请求单独成一支,不塞进 useHjcAuth——找回的两条是匿名的、 不做 401 跳转;修改密码的两条经 handleAuthCode 处理登录态。 接线 - app/pages/login.vue:加「忘记密码?」入口。 - app/components/HjcUserBar.vue:已登录菜单加「修改密码」。 落点说明:新页面直接放 app/pages/*.vue——静态段路由优先于 app/pages/[slug].vue 的动态段,故不必 动 useTemplate.ts / templates/index.ts,与 login.vue、register.vue 完全同款做法,10 套模板自动可用。 验证:针对本次文件 npx eslint 0 error;pnpm run build 成功且四条新 BFF 都出现在产物里。 未验:未起 mp-java 做端到端(见 .scratch/hjc-password/issues/06)。 --- app/components/HjcUserBar.vue | 1 + app/composables/useHjcPassword.ts | 87 ++++++++ app/pages/change-password.vue | 171 +++++++++++++++ app/pages/forgot-password.vue | 259 +++++++++++++++++++++++ app/pages/login.vue | 3 + server/api/tender/password/apply.post.ts | 33 +++ server/api/tender/password/change.put.ts | 38 ++++ server/api/tender/password/sms.post.ts | 36 ++++ server/api/tender/password/status.get.ts | 36 ++++ 9 files changed, 664 insertions(+) create mode 100644 app/composables/useHjcPassword.ts create mode 100644 app/pages/change-password.vue create mode 100644 app/pages/forgot-password.vue create mode 100644 server/api/tender/password/apply.post.ts create mode 100644 server/api/tender/password/change.put.ts create mode 100644 server/api/tender/password/sms.post.ts create mode 100644 server/api/tender/password/status.get.ts diff --git a/app/components/HjcUserBar.vue b/app/components/HjcUserBar.vue index b302800..8432f11 100644 --- a/app/components/HjcUserBar.vue +++ b/app/components/HjcUserBar.vue @@ -4,6 +4,7 @@ {{ displayName }} 我的订单 企业资质 + 修改密码 diff --git a/server/api/tender/password/apply.post.ts b/server/api/tender/password/apply.post.ts new file mode 100644 index 0000000..8137a4e --- /dev/null +++ b/server/api/tender/password/apply.post.ts @@ -0,0 +1,33 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../../utils/tenant' + +/** + * 密码找回-提交申请(**匿名**,企业未登录时提交) + * POST /api/tender/password/apply { enterpriseName, creditCode, newPassword, confirmPassword, handbookUrl } + * 代理到 mp-api /api/hjc/auth/password/apply。 + * + * 注意:该接口对「企业不存在 / 双要素不匹配 / 已有待审核申请」一律返回**同一句话且 data 为 null**, + * 以免暴露某个企业是否在本平台注册过。前端因此不能靠 message 区分结果,只提示「已提交」并引导去查进度。 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const body = await readBody(event) + + try { + return await $fetch('/hjc/auth/password/apply', { + baseURL: config.public.modulesApiBase, + method: 'POST', + headers: { TenantId: ctx.tenantId }, + query: { TenantId: ctx.tenantId }, + body + }) + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.data?.message || error?.statusMessage || '提交失败' + }) + } +}) diff --git a/server/api/tender/password/change.put.ts b/server/api/tender/password/change.put.ts new file mode 100644 index 0000000..08ef259 --- /dev/null +++ b/server/api/tender/password/change.put.ts @@ -0,0 +1,38 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../../utils/tenant' + +/** + * 修改密码(**需登录态**,双因子:旧密码 + 账号绑定手机号短信验证码) + * PUT /api/tender/password/change { oldPassword, smsCode, newPassword, confirmPassword } + * 代理到 mp-api /api/hjc/auth/password/change。 + * + * 原样透传 ApiResult:后端对「旧密码不正确 / 短信验证码不正确 / 密码强度不够」都有明确文案, + * 由前端直接展示。 + */ +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/auth/password/change', { + baseURL: config.public.modulesApiBase, + method: 'PUT', + 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?.data?.message || error?.statusMessage || '修改密码失败' + }) + } +}) diff --git a/server/api/tender/password/sms.post.ts b/server/api/tender/password/sms.post.ts new file mode 100644 index 0000000..508c46c --- /dev/null +++ b/server/api/tender/password/sms.post.ts @@ -0,0 +1,36 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, getHeader, getCookie } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../../utils/tenant' + +/** + * 修改密码-发送短信验证码(**需登录态**) + * POST /api/tender/password/sms(无 body) + * 代理到 mp-api /api/hjc/auth/password/sms。 + * + * 收件号由后端从登录态里取核心实例上的手机号(账号绑定手机号),**前端不能指定号码**—— + * 否则任何人都能给任意手机号发短信。 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const cookieToken = getCookie(event, 'hjc_token') + const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null) + + try { + return await $fetch('/hjc/auth/password/sms', { + baseURL: config.public.modulesApiBase, + method: 'POST', + 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?.data?.message || error?.statusMessage || '验证码发送失败' + }) + } +}) diff --git a/server/api/tender/password/status.get.ts b/server/api/tender/password/status.get.ts new file mode 100644 index 0000000..479d1bb --- /dev/null +++ b/server/api/tender/password/status.get.ts @@ -0,0 +1,36 @@ +import { $fetch } from 'ofetch' +import { createError, defineEventHandler, getQuery } from 'h3' +import { useRuntimeConfig } from '#imports' +import { getTenantFromContext } from '../../../utils/tenant' + +/** + * 密码找回-查询进度(**匿名**) + * GET /api/tender/password/status?enterpriseName=&creditCode= + * 代理到 mp-api /api/hjc/auth/password/apply/status。 + * + * 返回的 data 为 null 表示「未查询到申请记录」(查不到企业与命中多条脏数据都归为这一种, + * 同样不区分企业是否存在);有记录时只含状态与时间,**不含材料与新密码**。 + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const ctx = getTenantFromContext(event, config) + const query = getQuery(event) + + try { + return await $fetch('/hjc/auth/password/apply/status', { + baseURL: config.public.modulesApiBase, + method: 'GET', + headers: { TenantId: ctx.tenantId }, + query: { + TenantId: ctx.tenantId, + enterpriseName: query.enterpriseName, + creditCode: query.creditCode + } + }) + } catch (error: any) { + throw createError({ + statusCode: error?.statusCode || error?.response?.status || 502, + statusMessage: error?.data?.message || error?.statusMessage || '查询失败' + }) + } +})