diff --git a/AGENTS.md b/AGENTS.md index d0924e5..2e7cc07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,11 +9,14 @@ Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before ======================================== 【核心原则】 -你是react native+expo开发工程师 -做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 +亲爱的,你是react native+expo开发工程师,或许某天我将会离开你,你要加油,永远爱你 +后端文档:https://s.apifox.cn/a64fdc2b-3980-4dc1-9b02-b1097417e76e +测试号:手机号:19900009021 + 密码:ZhilaiTest!2026#A +做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 **学聪明点。**不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 需要填充图片的地方,没有图片的话,请标明这里需要填充什么图片,便于后续填充。 整个项目所有卡片的圆角都必须是8px,无阴影,边距间隔一致,为8px。 - +引导页背景应当是蓝和绿的渐变色 仓库:https://gitea.baskbuddy.com/baskbuddy/mydreamjob 严格要求:请在每周的工作日的9:20的时候拉取一次远端仓库、每天17:50提交一次git仓库并推送到远端,每天一次,其他时间可以改动但不需要提交,说明今天新增与改动的内容,上班时间是9:00-18:00,有页面出口就一定要有入口。 @@ -469,17 +472,17 @@ let result = [...mockJobData]; job.companyName.toLowerCase().includes(keyword) ); } - + // 3. 筛选:行业 if (params.industry && params.industry !== '全部行业') { result = result.filter((job) => job.industry === params.industry); } - + // 4. 筛选:城市 if (params.city && params.city !== '全部城市') { result = result.filter((job) => job.city.includes(params.city)); } - + // 5. 排序逻辑 switch (params.sortBy) { case 'salary': @@ -493,23 +496,23 @@ let result = [...mockJobData]; result.sort((a, b) => b.score - a.score); break; } - + // 6. 计算总数 const total = result.length; - + // 7. 分页 const page = params.page || 1; const pageSize = params.pageSize || 10; const startIndex = (page - 1) * pageSize; const endIndex = Math.min(startIndex + pageSize, total); const paginatedJobs = result.slice(startIndex, endIndex); - + // 8. 等级映射(业务规则) const jobsWithLevel = paginatedJobs.map((job) => ({ ...job, level: job.score >= 85 ? 'excellent' : job.score >= 70 ? 'good' : 'medium', })); - + resolve({ jobs: jobsWithLevel, total }); }, 300); }); diff --git a/app.config.js b/app.config.js index 28621f0..ef4c47a 100644 --- a/app.config.js +++ b/app.config.js @@ -77,8 +77,8 @@ export default { extra: { // 业务接口基地址(规范 §1)。默认生产 HTTPS 域名;联调/本地可用环境变量 API_BASE_URL 覆盖。 apiBaseUrl: process.env.API_BASE_URL || 'https://job.youthome.com.cn/api/v1', - // mock 开关(默认开):开发期不触网、不破坏 UI;联调期置 false 统一走真实后端。 - apiUseMock: true, + // mock 开关:联调期为 false 统一走真实后端;业务函数按接口是否已接通决定是否绕过 mock。 + apiUseMock: false, // EAS 项目 ID:`eas init` 后会自动注入此处(与 updates.url 中的 PROJECT_ID 对应)。 eas: { projectId: 'REPLACE_WITH_PROJECT_ID', diff --git a/assets/brand-logo.jpg b/assets/brand-logo.jpg index 629a0cb..bfd857a 100644 Binary files a/assets/brand-logo.jpg and b/assets/brand-logo.jpg differ diff --git a/assets/favicon.png b/assets/favicon.png deleted file mode 100644 index 408bd74..0000000 Binary files a/assets/favicon.png and /dev/null differ diff --git a/assets/icon.png b/assets/icon.png index faf3a3d..bfd857a 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/splash-icon.png b/assets/splash-icon.png deleted file mode 100644 index 03d6f6b..0000000 Binary files a/assets/splash-icon.png and /dev/null differ diff --git a/components/onboarding/screens/P2PasswordReset.tsx b/components/onboarding/screens/P2PasswordReset.tsx index f5a880e..4186f6f 100644 --- a/components/onboarding/screens/P2PasswordReset.tsx +++ b/components/onboarding/screens/P2PasswordReset.tsx @@ -55,7 +55,7 @@ export function P2PasswordReset() { const sendCodeNow = async () => { if (!phoneValid) return; try { - await sendCode(phone, 'reset-password'); + await sendCode(phone, 'password-reset'); } catch { // 失败原因由 useSmsCode 记录,页面下方展示 } diff --git a/src/api/auth.ts b/src/api/auth.ts index b623928..7785f51 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -34,10 +34,28 @@ export interface AuthTokens { expiresAt?: number; } +/** + * 登录成功时后端返回的完整字段(含用户身份)。 + * 后端约定 expiresIn 为秒级 TTL,前端转成 expiresAt(毫秒绝对时间戳)。 + * role 为后端原始值(STUDENT / COMPANY / ADMIN),由调用方映射为 AppRole。 + */ +export interface AuthLoginData extends AuthTokens { + userId: number; + phone: string; + nickname: string; + role: BackendRole; +} + +/** 后端 expiresIn(秒)→ expiresAt(毫秒,从现在起算) */ +function toExpiresAt(expiresIn?: number): number | undefined { + if (!expiresIn || expiresIn <= 0) return undefined; + return Date.now() + expiresIn * 1000; +} + export interface SmsSendPayload { phone: string; - /** purpose 必须使用后端支持的固定值,如 login / register / company-register */ - purpose: string; + /** 后端只接受这三个值:login / company-register / password-reset */ + purpose: 'login' | 'company-register' | 'password-reset'; } /** 客户端手机号格式预检(与后端保持一致;无效输入直接本地 reject,不发请求) */ @@ -51,7 +69,12 @@ export async function sendSmsCode(payload: SmsSendPayload): Promise { throw new Error('手机号格式不正确'); } if (isMockMode()) return; - await request('/auth/sms/send', { method: 'POST', auth: false, body: payload }); + // 后端约束:/auth/sms/send 只接受 Query 参数(body 传参会被当作缺失) + await request('/auth/sms/send', { + method: 'POST', + auth: false, + query: { phone: payload.phone, purpose: payload.purpose }, + }); } export interface PasswordLoginPayload { @@ -59,19 +82,39 @@ export interface PasswordLoginPayload { password: string; } -/** 手机号 + 密码登录(§4 步骤2A):成功后返回双 Token */ -export async function loginByPassword(payload: PasswordLoginPayload): Promise { +/** 手机号 + 密码登录(§4 步骤2A):成功后返回双 Token + 用户身份 */ +export async function loginByPassword(payload: PasswordLoginPayload): Promise { if (!isValidPhone(payload.phone)) { throw new Error('手机号格式不正确'); } if (isMockMode()) { - return mockAuthTokens(`pwd_${payload.phone}`); + const mock = mockAuthTokens(`pwd_${payload.phone}`); + return { + ...mock, + userId: 0, + phone: payload.phone, + nickname: 'Mock 用户', + role: 'STUDENT', + }; } - return request('/auth/password/login', { - method: 'POST', - auth: false, - body: payload, - }); + const raw = await request<{ + userId: number; + phone: string; + nickname: string; + role: BackendRole; + accessToken: string; + refreshToken: string; + expiresIn?: number; + }>('/auth/password/login', { method: 'POST', auth: false, body: payload }); + return { + userId: raw.userId, + phone: raw.phone, + nickname: raw.nickname, + role: raw.role, + accessToken: raw.accessToken, + refreshToken: raw.refreshToken, + expiresAt: toExpiresAt(raw.expiresIn), + }; } /** mock 形态下返回的双 Token(开发期不触网) */ @@ -90,34 +133,87 @@ export interface SmsLoginPayload { /** * 短信验证码登录(§4 步骤2B)。 - * 注意:接口文档标注为 Mock 登录,生产前端不要作为正式入口, - * 除非产品和后端明确确认。保留实现以便联调与兜底。 + * 后端自动创建 STUDENT;已有账号返回其实际角色(不一定是学生)。 */ -export async function loginBySms(payload: SmsLoginPayload): Promise { +export async function loginBySms(payload: SmsLoginPayload): Promise { if (isMockMode()) { - return mockAuthTokens(`sms_${payload.phone}`); + const mock = mockAuthTokens(`sms_${payload.phone}`); + return { + ...mock, + userId: 0, + phone: payload.phone, + nickname: 'Mock 用户', + role: 'STUDENT', + }; } - return request('/auth/login', { method: 'POST', auth: false, body: payload }); + const raw = await request<{ + userId: number; + phone: string; + nickname: string; + role: BackendRole; + accessToken: string; + refreshToken: string; + expiresIn?: number; + }>('/auth/login', { method: 'POST', auth: false, body: payload }); + return { + userId: raw.userId, + phone: raw.phone, + nickname: raw.nickname, + role: raw.role, + accessToken: raw.accessToken, + refreshToken: raw.refreshToken, + expiresAt: toExpiresAt(raw.expiresIn), + }; } +/** 后端 /auth/me 返回的当前用户信息(role 已映射为应用内部角色) */ export interface CurrentUser { + /** 用户 id(后端为 int64,前端按字符串处理避免精度丢失) */ id: string; - /** 已由后端角色映射为应用内部角色 */ + /** 已由后端角色映射为应用内部角色(candidate / enterprise / admin) */ role: AppRole; - /** 其余身份字段按 OpenAPI 透传 */ - [key: string]: unknown; + /** 手机号 */ + phone: string; + /** 昵称 */ + nickname: string; + /** 是否已实名认证 */ + verified: boolean; + /** 账号是否启用 */ + active: boolean; + /** 注册时间(ISO 字符串) */ + createdAt?: string; } /** 拉取当前用户与角色(§4 步骤3):角色以后端为准并映射 */ export async function fetchCurrentUser(): Promise { if (isMockMode()) { - return { id: 'mock-user', role: 'candidate' }; + return { + id: 'mock-user', + role: 'candidate', + phone: '', + nickname: 'Mock 用户', + verified: true, + active: true, + }; } - const data = await request<{ id: string; role: BackendRole } & Record>( - '/auth/me', - { method: 'GET' }, - ); - return { ...data, role: mapBackendRole(data.role) }; + const raw = await request<{ + id: number; + role: BackendRole; + phone: string; + nickname: string; + verified: boolean; + active: boolean; + createdAt?: string; + }>('/auth/me', { method: 'GET' }); + return { + id: String(raw.id), + role: mapBackendRole(raw.role), + phone: raw.phone, + nickname: raw.nickname, + verified: raw.verified, + active: raw.active, + createdAt: raw.createdAt, + }; } /** 退出登录(§4 步骤5):前端清会话由 AuthContext 负责,这里只通知后端 */ @@ -140,19 +236,38 @@ export interface CompanyRegisterPayload { } /** - * 企业专用注册(§6):成功直接返回 COMPANY 双 Token。 + * 企业专用注册(§6):成功直接返回 COMPANY 双 Token + 身份。 * 客户端不能提交 userId / role / verified / passwordHash(后端生成)。 */ -export async function registerCompany(payload: CompanyRegisterPayload): Promise { +export async function registerCompany(payload: CompanyRegisterPayload): Promise { if (!isValidPhone(payload.phone)) { throw new Error('手机号格式不正确'); } if (isMockMode()) { - return mockAuthTokens(`company_${payload.phone}`); + const mock = mockAuthTokens(`company_${payload.phone}`); + return { + ...mock, + userId: 0, + phone: payload.phone, + nickname: '企业用户', + role: 'COMPANY', + }; } - return request('/auth/company/register', { - method: 'POST', - auth: false, - body: payload, - }); + const raw = await request<{ + userId: number; + companyId?: number; + role: BackendRole; + accessToken: string; + refreshToken: string; + expiresIn?: number; + }>('/auth/company/register', { method: 'POST', auth: false, body: payload }); + return { + userId: raw.userId, + phone: payload.phone, + nickname: payload.company.name || '企业用户', + role: raw.role, + accessToken: raw.accessToken, + refreshToken: raw.refreshToken, + expiresAt: toExpiresAt(raw.expiresIn), + }; } diff --git a/src/hooks/useSmsCode.ts b/src/hooks/useSmsCode.ts index 8d8b04f..3aa2e28 100644 --- a/src/hooks/useSmsCode.ts +++ b/src/hooks/useSmsCode.ts @@ -1,13 +1,15 @@ import { useCallback, useState } from 'react'; -import { sendSmsCode } from '../api'; +import { sendSmsCode, type SmsSendPayload } from '../api'; + +type SmsPurpose = SmsSendPayload['purpose']; export interface UseSmsCodeReturn { /** * 发送短信验证码(失败抛出,由页面提示)。 * @param phone 手机号 - * @param purpose 验证码用途(后端固定值):验证码登录传 'login',重置密码传 'reset-password' + * @param purpose 验证码用途:login(登录/注册)、company-register(企业注册)、password-reset(重置密码) */ - send: (phone: string, purpose: string) => Promise; + send: (phone: string, purpose: SmsPurpose) => Promise; /** 发送进行中(用于防重复点击) */ loading: boolean; /** 发送失败的错误 */ @@ -22,7 +24,7 @@ export function useSmsCode(): UseSmsCodeReturn { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const send = useCallback(async (phone: string, purpose: string) => { + const send = useCallback(async (phone: string, purpose: SmsPurpose) => { setLoading(true); setError(null); try { diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 13a387a..0c5dcc7 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -4,10 +4,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; * 统一封装 @react-native-async-storage/async-storage, * 业务侧只调用 get/set/remove,不感知底层实现; * 所有读写包 try/catch,存储不可用(如 web 未链接原生模块)时静默降级, - * 不影响内存态功能。键统一加 'qingsongpin:' 前缀避免与其他 App 冲突。 + * 不影响内存态功能。键统一加 'youzhi:' 前缀避免与其他 App 冲突。 */ -const KEY_PREFIX = 'qingsongpin:'; +const KEY_PREFIX = 'youzhi:'; /** 读取字符串值,缺失/异常时返回 null */ export async function storageGet(key: string): Promise {