diff --git a/common/BuildProfile.ets b/common/BuildProfile.ets new file mode 100644 index 0000000..3a501e5 --- /dev/null +++ b/common/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0'; +export const BUILD_MODE_NAME = 'debug'; +export const DEBUG = true; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/common/Index.ets b/common/Index.ets new file mode 100644 index 0000000..64530f6 --- /dev/null +++ b/common/Index.ets @@ -0,0 +1 @@ +export * from './src/main/ets/Index.ets'; diff --git a/common/build-profile.json5 b/common/build-profile.json5 new file mode 100644 index 0000000..79961f9 --- /dev/null +++ b/common/build-profile.json5 @@ -0,0 +1,10 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "targets": [ + { + "name": "default" + } + ] +} diff --git a/common/hvigorfile.ts b/common/hvigorfile.ts new file mode 100644 index 0000000..08f99a1 --- /dev/null +++ b/common/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/common/oh-package.json5 b/common/oh-package.json5 new file mode 100644 index 0000000..16ba476 --- /dev/null +++ b/common/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "name": "common", + "version": "1.0.0", + "description": "基础设施层:网络 / 日志 / 状态 / 工具", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": {} +} diff --git a/common/src/main/ets/Index.ets b/common/src/main/ets/Index.ets new file mode 100644 index 0000000..48bf07e --- /dev/null +++ b/common/src/main/ets/Index.ets @@ -0,0 +1,8 @@ +export { HttpClient } from './network/HttpClient'; +export { HttpInterceptor } from './network/HttpInterceptor'; +export { AuthInterceptor } from './network/AuthInterceptor'; +export { ApiError } from './network/ApiError'; +export { ApiResult } from './network/ApiResult'; +export { SessionStore, SessionManager } from './state/SessionStore'; +export { Logger } from './logging/Logger'; +export { FormatUtil } from './utils/FormatUtil'; diff --git a/common/src/main/ets/logging/Logger.ets b/common/src/main/ets/logging/Logger.ets new file mode 100644 index 0000000..8a8d792 --- /dev/null +++ b/common/src/main/ets/logging/Logger.ets @@ -0,0 +1,24 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; + +const APP_DOMAIN: number = 0x0000; + +/** + * 日志封装:统一 DOMAIN 与等级,后续可在此加开关/上报。 + */ +export class Logger { + static debug(tag: string, message: string): void { + hilog.debug(APP_DOMAIN, tag, '%{public}s', message); + } + + static info(tag: string, message: string): void { + hilog.info(APP_DOMAIN, tag, '%{public}s', message); + } + + static warn(tag: string, message: string): void { + hilog.warn(APP_DOMAIN, tag, '%{public}s', message); + } + + static error(tag: string, message: string): void { + hilog.error(APP_DOMAIN, tag, '%{public}s', message); + } +} diff --git a/common/src/main/ets/network/ApiError.ets b/common/src/main/ets/network/ApiError.ets new file mode 100644 index 0000000..e94ee73 --- /dev/null +++ b/common/src/main/ets/network/ApiError.ets @@ -0,0 +1,26 @@ +/** + * 统一错误模型:业务错误码 / HTTP 状态码 / 网络异常。 + * 继承 Error 以符合 ArkTS 的 throw 限制(只能抛 Error 及其子类)。 + */ +export class ApiError extends Error { + readonly code: number; + readonly isNetworkError: boolean; + + constructor(code: number, message: string, isNetworkError: boolean = false) { + super(message); + this.code = code; + this.isNetworkError = isNetworkError; + } + + static network(detail: string): ApiError { + return new ApiError(-1, `网络请求失败: ${detail}`, true); + } + + static http(statusCode: number): ApiError { + return new ApiError(statusCode, `HTTP 状态码异常: ${statusCode}`, false); + } + + static business(code: number, message: string): ApiError { + return new ApiError(code, message, false); + } +} diff --git a/common/src/main/ets/network/ApiResult.ets b/common/src/main/ets/network/ApiResult.ets new file mode 100644 index 0000000..5772e39 --- /dev/null +++ b/common/src/main/ets/network/ApiResult.ets @@ -0,0 +1,18 @@ +/** + * 统一响应包装:与后端约定 code / message / data 结构。 + */ +export class ApiResult { + code: number; + message: string; + data: T; + + constructor(code: number, message: string, data: T) { + this.code = code; + this.message = message; + this.data = data; + } + + isSuccess(): boolean { + return this.code === 0; + } +} diff --git a/common/src/main/ets/network/AuthInterceptor.ets b/common/src/main/ets/network/AuthInterceptor.ets new file mode 100644 index 0000000..715f90e --- /dev/null +++ b/common/src/main/ets/network/AuthInterceptor.ets @@ -0,0 +1,24 @@ +import { HttpInterceptor } from './HttpInterceptor'; +import { SessionManager } from '../state/SessionStore'; + +/** + * 鉴权拦截器:请求前注入 Authorization 头;401 时清空会话(登出)。 + * + * TODO(鉴权): 401 时的 token 刷新 + 原请求重放(防并发刷新)需在 + * 后端 refresh 接口约定后实现,当前先清空会话交由上层跳登录。 + */ +export class AuthInterceptor implements HttpInterceptor { + onRequest(header: Record): Record { + const session = SessionManager.get(); + if (session.token.length > 0) { + header['Authorization'] = `Bearer ${session.token}`; + } + return header; + } + + onResponse(responseCode: number, result: string): void { + if (responseCode === 401) { + SessionManager.get().clear(); + } + } +} diff --git a/common/src/main/ets/network/HttpClient.ets b/common/src/main/ets/network/HttpClient.ets new file mode 100644 index 0000000..c2a4c84 --- /dev/null +++ b/common/src/main/ets/network/HttpClient.ets @@ -0,0 +1,65 @@ +import { http } from '@kit.NetworkKit'; +import { HttpInterceptor } from './HttpInterceptor'; +import { ApiError } from './ApiError'; + +/** + * HTTP 客户端封装:统一超时、请求头、拦截器链。 + * 基于 @kit.NetworkKit 的 http 模块(Promise 风格)。 + */ +export class HttpClient { + private readonly client: http.HttpRequest = http.createHttp(); + private interceptors: HttpInterceptor[] = []; + + addInterceptor(interceptor: HttpInterceptor): HttpClient { + this.interceptors.push(interceptor); + return this; + } + + async get(url: string, header?: Record): Promise { + return this.request(url, http.RequestMethod.GET, undefined, header); + } + + async post(url: string, body: string, header?: Record): Promise { + return this.request(url, http.RequestMethod.POST, body, header); + } + + async put(url: string, body: string, header?: Record): Promise { + return this.request(url, http.RequestMethod.PUT, body, header); + } + + async delete(url: string, header?: Record): Promise { + return this.request(url, http.RequestMethod.DELETE, undefined, header); + } + + private async request(url: string, method: http.RequestMethod, body?: string, + header?: Record): Promise { + let headers: Record = {}; + if (header !== undefined) { + headers = header; + } + for (const interceptor of this.interceptors) { + headers = interceptor.onRequest(headers); + } + + const options: http.HttpRequestOptions = { + method: method, + header: headers, + expectDataType: http.HttpDataType.STRING, + connectTimeout: 10000, + readTimeout: 10000 + }; + if (body !== undefined) { + options.extraData = body; + } + + try { + const response: http.HttpResponse = await this.client.request(url, options); + for (const interceptor of this.interceptors) { + interceptor.onResponse(response.responseCode, response.result as string); + } + return response.result as string; + } catch (err) { + throw ApiError.network(JSON.stringify(err)); + } + } +} diff --git a/common/src/main/ets/network/HttpInterceptor.ets b/common/src/main/ets/network/HttpInterceptor.ets new file mode 100644 index 0000000..acb9978 --- /dev/null +++ b/common/src/main/ets/network/HttpInterceptor.ets @@ -0,0 +1,10 @@ +/** + * 请求/响应拦截器接口。 + * 实现类可注入请求头(鉴权)、统一处理响应(401/错误码)等。 + */ +export interface HttpInterceptor { + /** 请求发出前,返回(可修改的)请求头。 */ + onRequest(header: Record): Record; + /** 响应返回后,可据此抛错中断(如 401 触发登出)。 */ + onResponse(responseCode: number, result: string): void; +} diff --git a/common/src/main/ets/state/SessionStore.ets b/common/src/main/ets/state/SessionStore.ets new file mode 100644 index 0000000..4ef336a --- /dev/null +++ b/common/src/main/ets/state/SessionStore.ets @@ -0,0 +1,56 @@ +import { PersistenceV2 } from '@kit.ArkUI'; + +/** + * 会话状态:登录 token / 用户信息。 + * 用 PersistenceV2 持久化,@Trace 字段变更自动落盘。 + */ +@ObservedV2 +export class SessionStore { + @Trace token: string = ''; + @Trace refreshToken: string = ''; + @Trace userId: string = ''; + @Trace displayName: string = ''; + + get isLoggedIn(): boolean { + return this.token.length > 0; + } + + setSession(token: string, refreshToken: string, userId: string, displayName: string): void { + this.token = token; + this.refreshToken = refreshToken; + this.userId = userId; + this.displayName = displayName; + } + + clear(): void { + this.token = ''; + this.refreshToken = ''; + this.userId = ''; + this.displayName = ''; + } +} + +/** + * 会话单例管理:全局唯一连接,跨页面/跨组件共享同一份持久化状态。 + */ +export class SessionManager { + private static instance: SessionStore | undefined = undefined; + + static init(): SessionStore { + if (SessionManager.instance === undefined) { + SessionManager.instance = PersistenceV2.globalConnect({ + type: SessionStore, + key: 'session', + defaultCreator: () => new SessionStore() + })!; + } + return SessionManager.instance; + } + + static get(): SessionStore { + if (SessionManager.instance === undefined) { + return SessionManager.init(); + } + return SessionManager.instance; + } +} diff --git a/common/src/main/ets/utils/FormatUtil.ets b/common/src/main/ets/utils/FormatUtil.ets new file mode 100644 index 0000000..052a0af --- /dev/null +++ b/common/src/main/ets/utils/FormatUtil.ets @@ -0,0 +1,15 @@ +/** + * 通用工具(占位示例,随业务扩充)。 + */ +export class FormatUtil { + static isEmpty(value: string): boolean { + return value.length === 0; + } + + static formatCount(count: number): string { + if (count >= 10000) { + return `${(count / 10000).toFixed(1)}w`; + } + return `${count}`; + } +} diff --git a/common/src/main/module.json5 b/common/src/main/module.json5 new file mode 100644 index 0000000..0cb5da6 --- /dev/null +++ b/common/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "common", + "type": "har", + "description": "$string:module_desc", + "deviceTypes": [ + "phone" + ] + } +} diff --git a/common/src/main/resources/base/element/string.json b/common/src/main/resources/base/element/string.json new file mode 100644 index 0000000..3e3e3c6 --- /dev/null +++ b/common/src/main/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "common module" + } + ] +}