diff --git a/server/dukang-api/src/modules/iam/auth.service.ts b/server/dukang-api/src/modules/iam/auth.service.ts index eee1a1c..a691063 100644 --- a/server/dukang-api/src/modules/iam/auth.service.ts +++ b/server/dukang-api/src/modules/iam/auth.service.ts @@ -304,12 +304,34 @@ export class AuthService { } } + /** + * 合伙人账号可登录态校验(统一闸门): + * 1) 账号自身 status !== ACTIVE(DISABLED,含子账号)→ 拦截 + * 2) 所属主账号 bindingStatus !== ACTIVE(PAUSED,城市合伙人绑定暂停)→ 拦截 + * 子账号的绑定状态以父主账号为准。 + */ + private async assertPartnerAccountActive(account: { + id: bigint; + status: string; + isPrimary: number; + bindingStatus?: string | null; + }): Promise { + if (account.status !== 'ACTIVE') { + throw new BadRequestException('该账号已暂停使用,请联系客服人员'); + } + const primary = + account.isPrimary === 1 ? account : await this.resolvePrimaryAccount(account.id); + if (primary.bindingStatus && primary.bindingStatus !== 'ACTIVE') { + throw new BadRequestException('该合伙人合作已暂停,请联系客服人员'); + } + } + private async assertPartnerAccountByPhone(phone: string) { const account = await this.prisma.partnerAccount.findUnique({ where: { phone }, }); if (!account) throw new BadRequestException('未找到合伙人账号'); - if (account.status !== 'ACTIVE') throw new BadRequestException('该账号已暂停使用,请联系客服人员'); + await this.assertPartnerAccountActive(account); return account; } @@ -1061,7 +1083,7 @@ export class AuthService { where: { phone: normalizedPhone }, }); if (!account) throw new BadRequestException('未找到合伙人账号'); - if (account.status !== 'ACTIVE') throw new BadRequestException('该账号已暂停使用,请联系客服人员'); + await this.assertPartnerAccountActive(account); await this.syncTestFlagByPhone(normalizedPhone); const primary = await this.resolvePrimaryAccount(account.id); await this.prisma.partnerAccount.update({ @@ -1723,9 +1745,7 @@ export class AuthService { throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信'); } - if (account.status !== 'ACTIVE') { - throw new BadRequestException('该账号已暂停使用,请联系客服人员'); - } + await this.assertPartnerAccountActive(account); account = await this.prisma.partnerAccount.update({ where: { id: account.id }, diff --git a/杜康好客-v3.4.18-门店体验与登录态优化.md b/杜康好客-v3.4.18-门店体验与登录态优化.md index fecc2ca..9f9e573 100644 --- a/杜康好客-v3.4.18-门店体验与登录态优化.md +++ b/杜康好客-v3.4.18-门店体验与登录态优化.md @@ -9,7 +9,7 @@ |----|------| | A. mini-user 门店座机电话脱敏 | `maskPhone` 统一「中间四位隐藏」;门店详情已调用 | | B. mini-user 套餐详情图 1/5 + 自动轮播 | `ProductCarousel` detail 变体 autoplay + 右下角 `1/5` 计数 | -| C. 合伙人账号暂停禁止登录 | 非 `ACTIVE` → 登录/发码均拒,提示「该账号已暂停使用,请联系客服人员」 | +| C. 合伙人账号暂停禁止登录(双拦截) | 账号 `status=DISABLED` 或主账号 `bindingStatus=PAUSED` → 登录/发码/微信均拒,提示「该账号已暂停使用」或「该合伙人合作已暂停」 | | D. 门店端 申请提现 / 筛选栏留白对齐 | `WithdrawPage` 左右内边距统一 `--space-page` | | E. 门店端 门店休息中 → 是否开启营业 | 门店 PAUSED 时点击首页「扫码核销」即弹「是否开启营业?」,不进入扫码流程 | | F. mini-user 门店套餐图片自适应完整显示 | `ProductCarousel` 新增 `imageFit="adaptive"`(widthFix + 动态高度),门店套餐详情不再裁剪 | @@ -38,16 +38,29 @@ - 图片容器 `.store-package-detail-gallery` 预留右下角定位锚点(如需)。 - 无 API / schema 变更。 -## C. 合伙人账号暂停禁止登录 +## C. 合伙人账号暂停禁止登录(双拦截:账号停用 + 合伙人绑定暂停) -- 后端 `POST /partner/auth/login/sms`(`server/.../modules/iam/auth.service.ts` 的 `loginPartner` ~L1063、`assertPartnerAccountByPhone` ~L307):账号 `status !== 'ACTIVE'` 时当前抛「合伙人账号已停用」。 - - 改为抛出:`该账号已暂停使用,请联系客服人员`。 - - 同时覆盖发码预检 `assertSmsSendAllowed`(PARTNER_LOGIN 场景 ~L341)与 `checkPartnerPhone`,使「暂停」账号无法获取短信验证码。 +> 两个状态字段、两个枚举,**不要混**: +> | 维度 | 字段 | 枚举 | 取值 | +> |------|------|------|------| +> | 登录账号启用状态 | `partner_account.status` | `AccountStatus` | `ACTIVE` / `DISABLED` | +> | 城市合伙人绑定状态 | `partner_account.bindingStatus` | `CityPartnerStatus` | `ACTIVE` / `PAUSED` | +> 列表(`admin/partners`)里能看到的是 `bindingStatus`;主账号自身 `status` 不在列表返回体(仅子账号 `children[].status` 有)。 + +- 拦截规则(**主账号、子账号都拦**): + 1. 登录账号自身 `status !== 'ACTIVE'`(`DISABLED`,主账号 / 子账号均适用)→ 抛 `该账号已暂停使用,请联系客服人员`。 + 2. 所属**主账号** `bindingStatus !== 'ACTIVE'`(`PAUSED`,城市合伙人绑定暂停)→ 抛 `该合伙人合作已暂停,请联系客服人员`。 + - 子账号的绑定状态以其**父主账号**为准(`resolvePrimaryAccount`)。 +- 后端统一闸门:新增 `assertPartnerAccountActive(account)`(`auth.service.ts` ~L307 后),一次性校验以上两条;由以下入口复用: + - `assertPartnerAccountByPhone`(~L320):被 `checkPartnerPhone`(`POST /partner/auth/phone/check`)、发码预检 `assertSmsSendAllowed`(PARTNER_LOGIN / PARTNER_PROXY_ORDER 场景)调用 → 暂停账号在「手机号校验」阶段即被拦截,无法获取短信验证码。 + - `loginPartner`(`POST /partner/auth/login/sms`,~L1063 后)。 + - `loginPartnerWechat`(`POST /partner/auth/login/wechat`,~L1726 后)。 - 前端 `apps/h5-partner/src/pages/LoginPage.tsx`: - - `formatPartnerError`(~L69):命中「已暂停 / 已停用」分支时返回 `该账号已暂停使用,请联系客服人员`。 - - `formatWechatError`(~L80):同上,覆盖微信登录路径。 -- 枚举现状:`AccountStatus` 仅 `ACTIVE` / `DISABLED`(`schema.prisma` ~L244)。本次**复用 `DISABLED` 即视为暂停**,不新增枚举,避免迁移;若后续需区分「暂停(可恢复)」与「停用(永久)」,再评估新增 `SUSPENDED`。 -- 统一文案:`该账号已暂停使用,请联系客服人员`。 + - `formatPartnerError`(~L69)/ `formatWechatError`(~L84):命中「已暂停 / 已停用」分支映射 `该账号已暂停使用,请联系客服人员`;其余未匹配错误(含 `该合伙人合作已暂停…`)原样透出 `return text`。 +- 枚举现状:`AccountStatus`(`schema.prisma` ~L244)与 `CityPartnerStatus`(~L203)均为既有,本次**无任何 Prisma 迁移**。 +- 统一文案: + - 账号停用:`该账号已暂停使用,请联系客服人员` + - 合伙人绑定暂停:`该合伙人合作已暂停,请联系客服人员` ## D. 门店端 申请提现 / 筛选栏左右留白对齐 @@ -102,7 +115,9 @@ - [ ] 门店详情座机显示形如 `0379-12****78`(中间四位隐藏),手机号仍 `138****8000`;拨打为明文。 - [ ] 套餐详情图多张时右下角显示 `1/5` 分页计数,并自动轮播(约 3.5s 切换);单图不计数、不轮播;点击仍可预览。 -- [ ] 合伙人账号处于非 ACTIVE(暂停 / 停用)时:短信登录与微信登录均被拦截,提示「该账号已暂停使用,请联系客服人员」,无法进入。 +- [ ] 合伙人**账号** `status=DISABLED`(主账号或子账号)时:短信登录与微信登录均被拦截,提示「该账号已暂停使用,请联系客服人员」,无法进入。 +- [ ] 合伙人**绑定** `bindingStatus=PAUSED`(主账号)时:无论用主账号还是其任一子账号登录,均被拦截,提示「该合伙人合作已暂停,请联系客服人员」。 +- [ ] 以上拦截在「手机号校验(`phone/check`)」阶段即生效,暂停账号拿不到短信验证码。 - [ ] 门店管理(结算提现)页:申请提现按钮与筛选栏左右留白与其他区块一致(统一 `--space-page`),不再贴边。 - [ ] 门店休息中(PAUSED)时:点击门店端首页「扫码核销」按钮**立即**弹「门店目前休息中无法核销,是否开启营业?」,**不进入扫码流程**;确认开启后门店状态刷新、可再次扫码;取消则维持拦截;未过审门店开张被拒时给出对应提示。 - [ ] 门店套餐详情图片按真实比例完整显示、不再被裁切(轮播高度随图自适应);自动轮播与 `1/5` 计数仍正常。