Browse Source

refactor(api): 联调地基收尾(auth/双Token/角色/环境配置/品牌资源)

- auth.ts 登录链路对齐 /auth/password/login → /auth/me 角色权威化
- token-store 持久化 access+refresh;useSmsCode 接入真实短信
- storage.ts 键前缀收敛;P2PasswordReset 文案修正
- app.config.js 默认 apiBaseUrl 指向 job.youthome.com.cn/api/v1
- AGENTS.md 补充『提交后必须推送』约定
- 品牌资源:brand-logo.jpg 替换为有职 logo,icon.png 同步;清理 favicon/splash 冗余
master
doublex001 4 weeks ago
parent
commit
1b1f6e573d
  1. 9
      AGENTS.md
  2. 4
      app.config.js
  3. BIN
      assets/brand-logo.jpg
  4. BIN
      assets/favicon.png
  5. BIN
      assets/icon.png
  6. BIN
      assets/splash-icon.png
  7. 2
      components/onboarding/screens/P2PasswordReset.tsx
  8. 181
      src/api/auth.ts
  9. 10
      src/hooks/useSmsCode.ts
  10. 4
      src/lib/storage.ts

9
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,有页面出口就一定要有入口。

4
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',

BIN
assets/brand-logo.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/favicon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/splash-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

2
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 记录,页面下方展示
}

181
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<void> {
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<AuthTokens> {
/** 手机号 + 密码登录(§4 步骤2A):成功后返回双 Token + 用户身份 */
export async function loginByPassword(payload: PasswordLoginPayload): Promise<AuthLoginData> {
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<AuthTokens>('/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<AuthTokens> {
export async function loginBySms(payload: SmsLoginPayload): Promise<AuthLoginData> {
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<AuthTokens>('/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<CurrentUser> {
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<string, unknown>>(
'/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<AuthTokens> {
export async function registerCompany(payload: CompanyRegisterPayload): Promise<AuthLoginData> {
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<AuthTokens>('/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),
};
}

10
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<void>;
send: (phone: string, purpose: SmsPurpose) => Promise<void>;
/** 发送进行中(用于防重复点击) */
loading: boolean;
/** 发送失败的错误 */
@ -22,7 +24,7 @@ export function useSmsCode(): UseSmsCodeReturn {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const send = useCallback(async (phone: string, purpose: string) => {
const send = useCallback(async (phone: string, purpose: SmsPurpose) => {
setLoading(true);
setError(null);
try {

4
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<string | null> {

Loading…
Cancel
Save