diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java
index 1c1ddf91..d87efcc3 100644
--- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java
+++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java
@@ -374,14 +374,15 @@ public class AihrOrgSyncService {
if (!keywordValue.isBlank()) {
String like = "%" + keywordValue + "%";
where.append("""
- and (person_name like ? or ext_party_id like ? or position_name like ?
- or dept_name like ? or project_name like ?)
+ and (person_name like ? or ext_party_id like ? or person_phone like ?
+ or position_name like ? or dept_name like ? or project_name like ?)
""");
args.add(like);
args.add(like);
args.add(like);
args.add(like);
args.add(like);
+ args.add(like);
}
String project = clean(projectCode);
if (!project.isBlank()) {
diff --git a/mobile-uni/src/pages.json b/mobile-uni/src/pages.json
index bc16011e..b4745569 100644
--- a/mobile-uni/src/pages.json
+++ b/mobile-uni/src/pages.json
@@ -37,6 +37,12 @@
"navigationBarTitleText": "登录"
}
},
+ {
+ "path": "pages/auth/position/index",
+ "style": {
+ "navigationBarTitleText": "确认岗位"
+ }
+ },
{
"path": "pages/common/building/index",
"style": {
diff --git a/mobile-uni/src/pages/auth/login/index.vue b/mobile-uni/src/pages/auth/login/index.vue
index 5a1830d0..68f8c0be 100644
--- a/mobile-uni/src/pages/auth/login/index.vue
+++ b/mobile-uni/src/pages/auth/login/index.vue
@@ -41,6 +41,7 @@ import type { LoginResult } from '@/types/api';
import { apiRequest } from '@/services/api';
import { consumeLoginRedirect, getAuth, isLoggedIn, phoneKey, saveAuth, validatePhone } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
+import { goToMobileTarget, nextUserRouteAfterLogin } from '@/services/position';
const stored = getAuth();
const phone = ref(stored.phone || uni.getStorageSync(phoneKey) || '');
@@ -48,20 +49,9 @@ const smsCode = ref('');
const sending = ref(false);
const loggingIn = ref(false);
const message = ref('');
-const tabPages = new Set([
- '/pages/user/today/index',
- '/pages/user/practice/index',
- '/pages/user/sop/index',
- '/pages/user/profile/index'
-]);
-
-const goAfterLogin = () => {
+const goAfterLogin = async () => {
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
- if (tabPages.has(redirect)) {
- uni.switchTab({ url: redirect });
- return;
- }
- uni.redirectTo({ url: redirect });
+ goToMobileTarget(await nextUserRouteAfterLogin(redirect));
};
const sendCode = async () => {
@@ -115,7 +105,7 @@ const login = async () => {
throw new Error('登录响应缺少认证信息');
}
saveAuth(result, value);
- goAfterLogin();
+ await goAfterLogin();
} catch (error) {
message.value = error instanceof Error ? error.message : '登录失败';
} finally {
@@ -126,7 +116,7 @@ const login = async () => {
onShow(() => {
resetPageScroll();
if (isLoggedIn()) {
- goAfterLogin();
+ void goAfterLogin();
}
});
diff --git a/mobile-uni/src/pages/auth/position/index.vue b/mobile-uni/src/pages/auth/position/index.vue
new file mode 100644
index 00000000..6940f090
--- /dev/null
+++ b/mobile-uni/src/pages/auth/position/index.vue
@@ -0,0 +1,157 @@
+
+
+
+ 确认岗位
+ {{ phone }}
+
+
+ {{ message }}
+
+
+ 选择当前岗位
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-uni/src/pages/user/practice/index.vue b/mobile-uni/src/pages/user/practice/index.vue
index d9b1ee94..8d84ad04 100644
--- a/mobile-uni/src/pages/user/practice/index.vue
+++ b/mobile-uni/src/pages/user/practice/index.vue
@@ -177,6 +177,7 @@ import { answerDailyDrill, finishPractice, getPracticeAssignments, getPracticePr
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { searchKnowledge } from '@/services/knowledge';
import { resetPageScroll } from '@/services/navigation';
+import { ensureEmployeePosition } from '@/services/position';
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
@@ -675,11 +676,18 @@ const finish = async (force = false) => {
}
};
-onShow(() => {
+const handleShow = async () => {
resetPageScroll();
refreshAuthState();
+ if (loggedIn.value && !(await ensureEmployeePosition('/pages/user/practice/index'))) {
+ return;
+ }
void loadAssignments();
void loadPrepCard();
+};
+
+onShow(() => {
+ void handleShow();
});
diff --git a/mobile-uni/src/pages/user/profile/index.vue b/mobile-uni/src/pages/user/profile/index.vue
index c3056422..2c22d3a4 100644
--- a/mobile-uni/src/pages/user/profile/index.vue
+++ b/mobile-uni/src/pages/user/profile/index.vue
@@ -14,6 +14,26 @@
+
+
+
+ 岗位身份
+ {{ positionProfile.value }} · {{ positionProfile.heroTitle }}
+
+
+
+
+
+
+
成长摘要
@@ -117,9 +137,20 @@
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
-import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
+import {
+ clearAuth,
+ getAuth,
+ getPositionProfile,
+ getSelectedPositionProfile,
+ isLoggedIn,
+ rememberLoginRedirect,
+ saveSelectedPosition,
+ userPositionOptions,
+ type UserPosition
+} from '@/services/auth';
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
import { resetPageScroll } from '@/services/navigation';
+import { ensureEmployeePosition } from '@/services/position';
const loggedIn = ref(false);
const phone = ref('');
@@ -128,11 +159,14 @@ const message = ref('');
const profile = ref(null);
const history = ref([]);
const evidence = ref(null);
+const positionProfile = ref(getSelectedPositionProfile());
+const positionCards = userPositionOptions.map((item) => getPositionProfile(item));
const refreshAuth = () => {
const auth = getAuth();
loggedIn.value = isLoggedIn();
phone.value = auth.phone;
+ positionProfile.value = getSelectedPositionProfile();
};
const clearProfileData = () => {
@@ -147,6 +181,10 @@ const loadData = async () => {
clearProfileData();
return;
}
+ if (!(await ensureEmployeePosition('/pages/user/profile/index'))) {
+ return;
+ }
+ refreshAuth();
loading.value = true;
message.value = '';
try {
@@ -183,6 +221,13 @@ const logout = () => {
uni.showToast({ title: '已退出', icon: 'none' });
};
+const changePosition = (position: UserPosition) => {
+ const saved = saveSelectedPosition(position);
+ if (!saved) return;
+ positionProfile.value = getSelectedPositionProfile();
+ uni.showToast({ title: '岗位已更新', icon: 'none' });
+};
+
onShow(() => {
resetPageScroll();
void loadData();
@@ -214,6 +259,48 @@ onShow(() => {
min-width: 84px;
}
+.position-current {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.position-title {
+ display: block;
+ margin-top: 8px;
+ color: #2563eb;
+ font-size: 16px;
+ font-weight: 900;
+}
+
+.position-actions {
+ display: grid;
+ margin-top: 14px;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.position-chip {
+ display: flex;
+ min-height: 38px;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ background: #f8fafc;
+ color: #475569;
+ font-size: 13px;
+ font-weight: 900;
+ white-space: nowrap;
+}
+
+.position-chip.active {
+ border-color: #2563eb;
+ background: #eff6ff;
+ color: #1d4ed8;
+}
+
.score-row {
display: grid;
margin-top: 16px;
diff --git a/mobile-uni/src/pages/user/sop/index.vue b/mobile-uni/src/pages/user/sop/index.vue
index 5aafbf0d..fbd8984b 100644
--- a/mobile-uni/src/pages/user/sop/index.vue
+++ b/mobile-uni/src/pages/user/sop/index.vue
@@ -8,7 +8,7 @@
v-model="query"
class="textarea"
maxlength="200"
- placeholder="例如: 业主投诉噪音,生活顾问应该怎么处理?"
+ :placeholder="quickQuestionPlaceholder"
/>
@@ -109,14 +109,17 @@ import { onShow } from '@dcloudio/uni-app';
import type { PracticeAssignment, RoleHome } from '@/types/api';
import { getMobileHome } from '@/services/home';
import { getPracticeAssignments } from '@/services/practice';
-import { getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
+import { getAuth, getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
+import { ensureEmployeePosition } from '@/services/position';
const home = ref(null);
const assignments = ref([]);
const message = ref('');
const loggedIn = ref(false);
-const quickQuestions = ['催费话术', '投诉应对', '收费标准'];
+const positionProfile = ref(getSelectedPositionProfile());
+const quickQuestions = computed(() => positionProfile.value.quickQuestions);
+const primaryTipQuestion = computed(() => quickQuestions.value[0] || '岗位SOP');
const pendingSopQueryKey = 'aihr_sop_pending_query';
const pendingAssignmentKey = 'aihr_pending_assignment_id';
@@ -176,12 +179,17 @@ const mentorBubbleText = computed(() =>
const loadHome = async () => {
loggedIn.value = isLoggedIn();
+ positionProfile.value = getSelectedPositionProfile();
message.value = '';
assignments.value = [];
if (!loggedIn.value) {
home.value = null;
return;
}
+ if (!(await ensureEmployeePosition('/pages/user/today/index'))) {
+ return;
+ }
+ positionProfile.value = getSelectedPositionProfile();
const [homeResult, assignmentResult] = await Promise.allSettled([
getMobileHome('user'),
getPracticeAssignments(true)
diff --git a/mobile-uni/src/services/auth.ts b/mobile-uni/src/services/auth.ts
index 8ef168a9..5f153f04 100644
--- a/mobile-uni/src/services/auth.ts
+++ b/mobile-uni/src/services/auth.ts
@@ -4,18 +4,97 @@ export const tokenKey = 'aihr_mobile_access_token';
export const clientIdKey = 'aihr_mobile_client_id';
export const phoneKey = 'aihr_mobile_phone';
export const loginRedirectKey = 'aihr_mobile_login_redirect';
+export const positionKey = 'aihr_mobile_position';
export const phonePattern = /^1[3-9]\d{9}$/;
+export const userPositionOptions = ['生活顾问', '保安', '保洁', '保修', '客服'] as const;
+
+export type UserPosition = typeof userPositionOptions[number];
+
+export interface UserPositionProfile {
+ value: UserPosition;
+ heroTitle: string;
+ quickQuestions: string[];
+ tip: string;
+}
+
export interface AuthState {
token: string;
clientId: string;
phone: string;
+ position: UserPosition | '';
}
+const positionProfiles: Record = {
+ 生活顾问: {
+ value: '生活顾问',
+ heroTitle: '客服管家',
+ quickQuestions: ['催费话术', '投诉应对', '收费标准'],
+ tip: '今日小贴士:催费沟通先共情,再说明,再给方案。'
+ },
+ 保安: {
+ value: '保安',
+ heroTitle: '秩序岗',
+ quickQuestions: ['门岗核验', '巡逻异常', '车辆指引'],
+ tip: '今日小贴士:异常先确认人车物,再登记,再同步主管。'
+ },
+ 保洁: {
+ value: '保洁',
+ heroTitle: '环境岗',
+ quickQuestions: ['公区清洁', '垃圾分类', '消杀标准'],
+ tip: '今日小贴士:高频区域先看地面、扶手、电梯按钮。'
+ },
+ 保修: {
+ value: '保修',
+ heroTitle: '工程维修',
+ quickQuestions: ['报修接单', '上门话术', '安全检查'],
+ tip: '今日小贴士:上门前确认故障、工具、预约时间。'
+ },
+ 客服: {
+ value: '客服',
+ heroTitle: '服务前台',
+ quickQuestions: ['来访接待', '投诉登记', '工单跟进'],
+ tip: '今日小贴士:复杂诉求先复述确认,再记录责任人与时限。'
+ }
+};
+
+const cleanText = (value: unknown) => String(value || '').trim();
+
+export const normalizePosition = (value: unknown): UserPosition | '' => {
+ const text = cleanText(value);
+ if (!text) return '';
+ if (text.includes('生活顾问') || text.includes('客服管家') || text.includes('物业管家') || text.includes('管家')) {
+ return '生活顾问';
+ }
+ if (text.includes('保安') || text.includes('秩序') || text.includes('安保')) return '保安';
+ if (text.includes('保洁') || text.includes('环境')) return '保洁';
+ if (text.includes('保修') || text.includes('维修') || text.includes('工程')) return '保修';
+ if (text.includes('客服') || text.includes('前台')) return '客服';
+ return (userPositionOptions as readonly string[]).includes(text) ? (text as UserPosition) : '';
+};
+
+export const hasSelectedPosition = () => Boolean(normalizePosition(uni.getStorageSync(positionKey)));
+
+export const getSelectedPosition = (): UserPosition =>
+ normalizePosition(uni.getStorageSync(positionKey)) || '生活顾问';
+
+export const getPositionProfile = (position: unknown) =>
+ positionProfiles[normalizePosition(position) || '生活顾问'];
+
+export const getSelectedPositionProfile = () => getPositionProfile(getSelectedPosition());
+
+export const saveSelectedPosition = (position: unknown) => {
+ const normalized = normalizePosition(position);
+ if (!normalized) return '';
+ uni.setStorageSync(positionKey, normalized);
+ return normalized;
+};
+
export const getAuth = (): AuthState => ({
token: uni.getStorageSync(tokenKey) || '',
clientId: uni.getStorageSync(clientIdKey) || '',
- phone: uni.getStorageSync(phoneKey) || ''
+ phone: uni.getStorageSync(phoneKey) || '',
+ position: normalizePosition(uni.getStorageSync(positionKey))
});
export const isLoggedIn = () => {
@@ -24,6 +103,10 @@ export const isLoggedIn = () => {
};
export const saveAuth = (result: LoginResult, phone: string) => {
+ const previousPhone = uni.getStorageSync(phoneKey) || '';
+ if (previousPhone && previousPhone !== phone) {
+ uni.removeStorageSync(positionKey);
+ }
uni.setStorageSync(tokenKey, result.access_token);
uni.setStorageSync(clientIdKey, result.client_id);
uni.setStorageSync(phoneKey, phone);
@@ -33,6 +116,7 @@ export const clearAuth = () => {
uni.removeStorageSync(tokenKey);
uni.removeStorageSync(clientIdKey);
uni.removeStorageSync(phoneKey);
+ uni.removeStorageSync(positionKey);
uni.removeStorageSync(loginRedirectKey);
};
diff --git a/mobile-uni/src/services/knowledge.ts b/mobile-uni/src/services/knowledge.ts
index 71db047b..95971f93 100644
--- a/mobile-uni/src/services/knowledge.ts
+++ b/mobile-uni/src/services/knowledge.ts
@@ -5,6 +5,7 @@ import type {
SummaryCardResponse
} from '@/types/api';
import { apiRequest } from './api';
+import { getSelectedPosition } from './auth';
type RawKnowledgeSnippet = KnowledgeSnippet & {
title?: string;
@@ -24,6 +25,7 @@ export const searchKnowledge = async (queryText: string) => {
data: {
queryText,
category: 'sop',
+ position: getSelectedPosition(),
limit: 5
},
timeout: 30000
@@ -47,7 +49,7 @@ export const submitAnswerFeedback = (params: {
fragmentIds: params.fragmentIds,
verdict: params.verdict,
category: 'sop',
- position: '生活顾问',
+ position: getSelectedPosition(),
source: 'mobile_uni_sop'
},
timeout: 15000
diff --git a/mobile-uni/src/services/position.ts b/mobile-uni/src/services/position.ts
new file mode 100644
index 00000000..023aea7e
--- /dev/null
+++ b/mobile-uni/src/services/position.ts
@@ -0,0 +1,58 @@
+import type { OrgSnapshotResponse } from '@/types/api';
+import { apiRequest } from './api';
+import {
+ getAuth,
+ hasSelectedPosition,
+ normalizePosition,
+ saveSelectedPosition,
+ type UserPosition
+} from './auth';
+
+export const userTabPages = new Set([
+ '/pages/user/today/index',
+ '/pages/user/practice/index',
+ '/pages/user/sop/index',
+ '/pages/user/profile/index'
+]);
+
+export const isUserRoute = (url: string) => url.startsWith('/pages/user/');
+
+export const positionSetupUrl = (redirect: string) =>
+ `/pages/auth/position/index?redirect=${encodeURIComponent(redirect || '/pages/user/today/index')}`;
+
+export const goToMobileTarget = (url: string) => {
+ if (userTabPages.has(url)) {
+ uni.switchTab({ url });
+ return;
+ }
+ uni.redirectTo({ url });
+};
+
+export const hydratePositionFromOrg = async (phone = getAuth().phone): Promise => {
+ const keyword = phone.trim();
+ if (!keyword) return '';
+ try {
+ const snapshot = await apiRequest({
+ url: `/api/aihr/org/snapshot?keyword=${encodeURIComponent(keyword)}&status=active&pageSize=1`,
+ method: 'GET',
+ timeout: 15000
+ });
+ const officialPosition = snapshot.rows?.[0]?.positionName || '';
+ return saveSelectedPosition(officialPosition);
+ } catch {
+ return '';
+ }
+};
+
+export const nextUserRouteAfterLogin = async (redirect: string) => {
+ if (!isUserRoute(redirect) || hasSelectedPosition()) return redirect;
+ const officialPosition = await hydratePositionFromOrg();
+ return normalizePosition(officialPosition) ? redirect : positionSetupUrl(redirect);
+};
+
+export const ensureEmployeePosition = async (redirect: string) => {
+ const nextRoute = await nextUserRouteAfterLogin(redirect);
+ if (nextRoute === redirect) return true;
+ uni.navigateTo({ url: nextRoute });
+ return false;
+};
diff --git a/mobile-uni/src/services/practice.ts b/mobile-uni/src/services/practice.ts
index 706a7585..33813f47 100644
--- a/mobile-uni/src/services/practice.ts
+++ b/mobile-uni/src/services/practice.ts
@@ -14,7 +14,7 @@ import type {
PromotionEvidence
} from '@/types/api';
import { apiRequest } from './api';
-import { getAuth } from './auth';
+import { getAuth, getSelectedPosition } from './auth';
const defaultScenarioId = 'complaint-water';
@@ -40,6 +40,7 @@ export const startPractice = (scenarioId = defaultScenarioId, assignmentId?: num
data: {
extPartyId: currentExtPartyId(),
scenarioId,
+ position: getSelectedPosition(),
mode: 'mobile',
assignmentId
},
diff --git a/mobile-uni/src/types/api.ts b/mobile-uni/src/types/api.ts
index a22c206f..100133c7 100644
--- a/mobile-uni/src/types/api.ts
+++ b/mobile-uni/src/types/api.ts
@@ -48,6 +48,28 @@ export interface RoleHome {
todos?: TodoItem[];
}
+export interface OrgPersonRow {
+ id: number;
+ projectCode: string;
+ projectName: string;
+ deptName: string;
+ extPartyId: string;
+ personName: string;
+ positionName: string;
+ positionLevel: string;
+ employmentStatus: string;
+ snapshotDate: string;
+}
+
+export interface OrgSnapshotResponse {
+ total: number;
+ active: number;
+ departed: number;
+ pageNum: number;
+ pageSize: number;
+ rows: OrgPersonRow[];
+}
+
export interface KnowledgeSnippet {
docTitle: string;
content: string;