From e90b453b75cc3f91fe0e938b5cb2dbe37fa06bb1 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期四, 30 四月 2026 19:12:00 +0800
Subject: [PATCH] feat:邀请码回填问题
---
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppWechatLoginRequest.java | 2
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java | 33 +++++++++++
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java | 33 +++++++++++
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java | 5 +
traditionalApp/pages/login/login.vue | 8 ++
traditionalApp/utils/utils.js | 52 ++++++++++++-----
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/dto/AppSettingsPhoneBindRequest.java | 2
traditionalApp/pages/bindPhone/bindPhone.vue | 5 +
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/settings/service/AppSettingsServiceTest.java | 5 +
traditionalApp/utils/invite-cache.js | 9 ++
10 files changed, 135 insertions(+), 19 deletions(-)
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppWechatLoginRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppWechatLoginRequest.java
index 628ea67..f9b56e3 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppWechatLoginRequest.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppWechatLoginRequest.java
@@ -16,4 +16,6 @@
private String accessToken;
private String openid;
private String unionid;
+
+ private String inviteCode;
}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
index 2e2c384..bec0650 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
@@ -12,6 +12,7 @@
import com.tcm.training.admin.app.auth.vo.AppLoginUserVO;
import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
+import com.tcm.training.admin.app.share.service.AppShareService;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import cn.dev33.satoken.stp.SaTokenInfo;
@@ -41,6 +42,7 @@
private final AppAuthSessionService appAuthSessionService;
private final AppWechatAuthClient appWechatAuthClient;
private final AppAppleAuthClient appAppleAuthClient;
+ private final AppShareService appShareService;
private final StringRedisTemplate stringRedisTemplate;
/**
@@ -52,6 +54,7 @@
AppAuthSessionService appAuthSessionService,
AppWechatAuthClient appWechatAuthClient,
AppAppleAuthClient appAppleAuthClient,
+ AppShareService appShareService,
StringRedisTemplate stringRedisTemplate) {
this.userMapper = userMapper;
this.appSmsCodeService = appSmsCodeService;
@@ -59,6 +62,7 @@
this.appAuthSessionService = appAuthSessionService;
this.appWechatAuthClient = appWechatAuthClient;
this.appAppleAuthClient = appAppleAuthClient;
+ this.appShareService = appShareService;
this.stringRedisTemplate = stringRedisTemplate;
}
@@ -111,6 +115,7 @@
AppUser user = findUserByWechatOpenid(profile.getOpenid());
if (user == null) {
user = buildWechatUser(profile);
+ bindInviteRelationIfAbsent(user, request.getInviteCode());
userMapper.insert(user);
return createLoginResponse(user);
}
@@ -271,6 +276,22 @@
}
/**
+ * 用户尚未绑定诊所关系时,根据医生邀请码补齐上下级关系。
+ */
+ private void bindInviteRelationIfAbsent(AppUser user, String inviteCode) {
+ if (user.getClinicId() != null || user.getSuperiorClinicUserId() != null) {
+ return;
+ }
+ String normalizedInviteCode = normalizeInviteCode(inviteCode);
+ if (normalizedInviteCode == null) {
+ return;
+ }
+ AppShareService.InviteBindingTarget bindingTarget = appShareService.resolveInviteBindingTarget(normalizedInviteCode);
+ user.setClinicId(bindingTarget.getClinicId());
+ user.setSuperiorClinicUserId(bindingTarget.getSuperiorClinicUserId());
+ }
+
+ /**
* 基于苹果资料构造首次登录时的本地普通用户。
*/
private AppUser buildAppleUser(AppAppleUserProfile profile) {
@@ -367,6 +388,18 @@
}
/**
+ * 归一化医生邀请码,非法时忽略以保持第三方登录主流程可用。
+ */
+ private String normalizeInviteCode(String inviteCode) {
+ String normalized = trimToNull(inviteCode);
+ if (normalized == null) {
+ return null;
+ }
+ String upperInviteCode = normalized.toUpperCase();
+ return upperInviteCode.matches("^[A-Z0-9]{6}$") ? upperInviteCode : null;
+ }
+
+ /**
* 归一化并校验忘记密码场景下的新密码格式。
*/
private String normalizeNewPassword(String password) {
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/dto/AppSettingsPhoneBindRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/dto/AppSettingsPhoneBindRequest.java
index 43b4be6..ca3d034 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/dto/AppSettingsPhoneBindRequest.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/dto/AppSettingsPhoneBindRequest.java
@@ -19,4 +19,6 @@
@NotBlank(message = "请输入验证码")
private String code;
+
+ private String inviteCode;
}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java
index 0af4fed..a8c3ed9 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/settings/service/AppSettingsService.java
@@ -17,6 +17,7 @@
import com.tcm.training.admin.app.settings.vo.AppSettingsNotificationVO;
import com.tcm.training.admin.app.settings.vo.AppSettingsProfileVO;
import com.tcm.training.admin.app.settings.vo.AppSettingsWechatBindingVO;
+import com.tcm.training.admin.app.share.service.AppShareService;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import com.tcm.training.admin.upload.ObsUploadService;
@@ -54,6 +55,7 @@
private final AppSmsCodeService appSmsCodeService;
private final BCryptPasswordEncoder passwordEncoder;
private final AppWechatAuthClient appWechatAuthClient;
+ private final AppShareService appShareService;
private final AppUserNotificationSettingMapper appUserNotificationSettingMapper;
private final ObsUploadService obsUploadService;
@@ -65,6 +67,7 @@
AppSmsCodeService appSmsCodeService,
BCryptPasswordEncoder passwordEncoder,
AppWechatAuthClient appWechatAuthClient,
+ AppShareService appShareService,
AppUserNotificationSettingMapper appUserNotificationSettingMapper,
ObsUploadService obsUploadService) {
this.appAuthSessionService = appAuthSessionService;
@@ -72,6 +75,7 @@
this.appSmsCodeService = appSmsCodeService;
this.passwordEncoder = passwordEncoder;
this.appWechatAuthClient = appWechatAuthClient;
+ this.appShareService = appShareService;
this.appUserNotificationSettingMapper = appUserNotificationSettingMapper;
this.obsUploadService = obsUploadService;
}
@@ -157,8 +161,25 @@
requirePhoneAvailable(newPhone, user.getId());
appSmsCodeService.validatePhoneChangeCode(newPhone, code);
user.setPhone(newPhone);
+ bindInviteRelationIfAbsent(user, request.getInviteCode());
userMapper.updateById(user);
return toProfileVO(user);
+ }
+
+ /**
+ * 用户尚未绑定诊所关系时,根据医生邀请码补齐上下级关系。
+ */
+ private void bindInviteRelationIfAbsent(AppUser user, String inviteCode) {
+ if (user.getClinicId() != null || user.getSuperiorClinicUserId() != null) {
+ return;
+ }
+ String normalizedInviteCode = normalizeInviteCode(inviteCode);
+ if (normalizedInviteCode == null) {
+ return;
+ }
+ AppShareService.InviteBindingTarget bindingTarget = appShareService.resolveInviteBindingTarget(normalizedInviteCode);
+ user.setClinicId(bindingTarget.getClinicId());
+ user.setSuperiorClinicUserId(bindingTarget.getSuperiorClinicUserId());
}
/**
@@ -422,6 +443,18 @@
}
/**
+ * 归一化医生邀请码,非法时忽略以保持绑定手机号主流程可用。
+ */
+ private String normalizeInviteCode(String inviteCode) {
+ String normalized = trimToNull(inviteCode);
+ if (normalized == null) {
+ return null;
+ }
+ String upperInviteCode = normalized.toUpperCase();
+ return upperInviteCode.matches("^[A-Z0-9]{6}$") ? upperInviteCode : null;
+ }
+
+ /**
* 校验通知时间仅允许使用页面固定选项。
*/
private Integer normalizeAdvanceMinutes(Integer advanceMinutes) {
diff --git a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
index 6cd7be9..b3b383d 100644
--- a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
+++ b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
@@ -11,6 +11,7 @@
import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
import com.tcm.training.admin.app.auth.vo.AppPasswordResetVerifyResponse;
import com.tcm.training.admin.app.auth.vo.AppSmsCodeSendResponse;
+import com.tcm.training.admin.app.share.service.AppShareService;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import cn.dev33.satoken.stp.SaTokenInfo;
@@ -72,6 +73,9 @@
private AppAppleAuthClient appAppleAuthClient;
@Mock
+ private AppShareService appShareService;
+
+ @Mock
private StringRedisTemplate stringRedisTemplate;
@Mock
@@ -94,6 +98,7 @@
appAuthSessionService,
appWechatAuthClient,
appAppleAuthClient,
+ appShareService,
stringRedisTemplate
);
}
diff --git a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/settings/service/AppSettingsServiceTest.java b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/settings/service/AppSettingsServiceTest.java
index b695bf2..8300e88 100644
--- a/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/settings/service/AppSettingsServiceTest.java
+++ b/tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/settings/service/AppSettingsServiceTest.java
@@ -8,6 +8,7 @@
import com.tcm.training.admin.app.settings.dto.AppSettingsPhoneBindRequest;
import com.tcm.training.admin.app.settings.mapper.AppUserNotificationSettingMapper;
import com.tcm.training.admin.app.settings.vo.AppSettingsProfileVO;
+import com.tcm.training.admin.app.share.service.AppShareService;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import com.tcm.training.admin.upload.ObsUploadService;
@@ -58,6 +59,9 @@
private AppWechatAuthClient appWechatAuthClient;
@Mock
+ private AppShareService appShareService;
+
+ @Mock
private AppUserNotificationSettingMapper appUserNotificationSettingMapper;
@Mock
@@ -79,6 +83,7 @@
appSmsCodeService,
passwordEncoder,
appWechatAuthClient,
+ appShareService,
appUserNotificationSettingMapper,
obsUploadService
);
diff --git a/traditionalApp/pages/bindPhone/bindPhone.vue b/traditionalApp/pages/bindPhone/bindPhone.vue
index ac019cd..d2a0b18 100644
--- a/traditionalApp/pages/bindPhone/bindPhone.vue
+++ b/traditionalApp/pages/bindPhone/bindPhone.vue
@@ -45,6 +45,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { bindSettingsPhone, sendBindPhoneCode } from './service.js'
+import { clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
interface ApiResponse<T> {
code: number
@@ -166,12 +167,14 @@
try {
const response = await bindSettingsPhone({
newPhone: normalizedPhone,
- code: normalizedCode
+ code: normalizedCode,
+ inviteCode: getCachedInviteCode()
})
if (!isSuccessResponse(response)) {
return
}
cacheBoundPhone(response.data, normalizedPhone)
+ clearCachedInviteCode()
uni.showToast({
title: '绑定成功',
icon: 'success'
diff --git a/traditionalApp/pages/login/login.vue b/traditionalApp/pages/login/login.vue
index f11e021..d44b0d9 100644
--- a/traditionalApp/pages/login/login.vue
+++ b/traditionalApp/pages/login/login.vue
@@ -62,6 +62,7 @@
import { computed, onUnmounted, ref } from 'vue'
import { appleLogin, passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
import { syncJPushAliasWithCurrentUser } from '@/utils/push.js'
+ import { clearCachedInviteCode, getCachedInviteCode } from '@/utils/invite-cache.js'
const loginType = ref<'code' | 'password'>('code')
const phone = ref('')
@@ -104,6 +105,7 @@
accessToken ?: string
openid ?: string
unionid ?: string
+ inviteCode ?: string
}
interface AppAppleLoginPayload {
@@ -508,7 +510,8 @@
code: normalizeOptionalText(loginRes?.code),
accessToken: normalizeOptionalText(authResult.access_token || authResult.accessToken),
openid: normalizeOptionalText(authResult.openid || authResult.openId),
- unionid: normalizeOptionalText(authResult.unionid || authResult.unionId)
+ unionid: normalizeOptionalText(authResult.unionid || authResult.unionId),
+ inviteCode: getCachedInviteCode()
}
// 当前阶段优先兼容 APP 端直接返回的 accessToken/openid,后续补齐配置后也可走 code 模式。
@@ -645,6 +648,9 @@
uni.setStorageSync('token', authToken)
uni.setStorageSync('appUserInfo', loginData.user || {})
uni.setStorageSync('showLogoutModal', 1)
+ if (loginData.user?.clinicId) {
+ clearCachedInviteCode()
+ }
// 先保留来源参数,后续如需登录后回跳可直接复用。
uni.setStorageSync('loginRedirectMeta', {
inviteUserId: inviteUserId.value,
diff --git a/traditionalApp/utils/invite-cache.js b/traditionalApp/utils/invite-cache.js
index e72e208..ea4d6a7 100644
--- a/traditionalApp/utils/invite-cache.js
+++ b/traditionalApp/utils/invite-cache.js
@@ -15,10 +15,15 @@
}
/**
- * 读取本地缓存的邀请码,只返回合法的 6 位业务码。
+ * 读取本地缓存的邀请码,只返回合法的 6 位业务码,历史误写入值会自动清理。
*/
export const getCachedInviteCode = () => {
- return extractInviteCode(uni.getStorageSync(INVITE_CODE_CACHE_KEY))
+ const cachedValue = uni.getStorageSync(INVITE_CODE_CACHE_KEY)
+ const inviteCode = extractInviteCode(cachedValue)
+ if (!inviteCode && cachedValue) {
+ clearCachedInviteCode()
+ }
+ return inviteCode
}
/**
diff --git a/traditionalApp/utils/utils.js b/traditionalApp/utils/utils.js
index 7032f75..ba40388 100644
--- a/traditionalApp/utils/utils.js
+++ b/traditionalApp/utils/utils.js
@@ -258,6 +258,10 @@
* 从 URL、纯文本、对象或扫码内容中提取 6 位邀请码。
*/
export const extractInviteCode = (value) => {
+ const directInviteCode = extractObjectFieldFromAny(value, 'inviteCode')
+ if (directInviteCode) {
+ return normalizeInviteCodeText(directInviteCode)
+ }
const normalizedValue = normalizeInvitePayloadText(value)
if (!normalizedValue) {
return ''
@@ -270,8 +274,7 @@
if (queryMatchedInviteCode && queryMatchedInviteCode[1]) {
return normalizeInviteCodeText(decodeURIComponent(queryMatchedInviteCode[1]))
}
- const matchedInviteCode = normalizedValue.toUpperCase().match(/[A-Z0-9]{6}/)
- return matchedInviteCode ? matchedInviteCode[0] : ''
+ return normalizeInviteCodeText(normalizedValue)
}
/**
@@ -294,26 +297,45 @@
}
/**
+ * 从对象或 JSON 字符串中读取指定字段。
+ */
+const extractObjectFieldFromAny = (value, fieldName) => {
+ if (!value) {
+ return ''
+ }
+ if (typeof value === 'string') {
+ return extractObjectField(value, fieldName)
+ }
+ if (typeof value !== 'object') {
+ return ''
+ }
+ const directValue = value[fieldName]
+ if (typeof directValue === 'string') {
+ return directValue
+ }
+ const candidateKeys = ['bindData', 'data', 'channelData', 'params', 'wakeUpData']
+ for (const key of candidateKeys) {
+ const nestedValue = value[key]
+ if (!nestedValue || nestedValue === value) {
+ continue
+ }
+ const nestedFieldValue = extractObjectFieldFromAny(nestedValue, fieldName)
+ if (nestedFieldValue) {
+ return nestedFieldValue
+ }
+ }
+ return ''
+}
+
+/**
* 从 JSON 字符串中读取指定字段。
*/
const extractObjectField = (value, fieldName) => {
try {
- const parsedValue = JSON.parse(value)
- if (!parsedValue || typeof parsedValue !== 'object') {
- return ''
- }
- const directValue = parsedValue[fieldName]
- if (typeof directValue === 'string') {
- return directValue
- }
- const bindDataValue = parsedValue.bindData || parsedValue.data || parsedValue.params
- if (bindDataValue && bindDataValue !== value) {
- return extractObjectField(normalizeInvitePayloadText(bindDataValue), fieldName)
- }
+ return extractObjectFieldFromAny(JSON.parse(value), fieldName)
} catch {
return ''
}
- return ''
}
/**
--
Gitblit v1.7.1