feat(common): 新增基础设施 HAR(网络/鉴权/日志/状态/工具)

This commit is contained in:
AndyChen1024
2026-09-11 16:22:45 +08:00
parent 2dc02b44c7
commit b73e74fa4a
16 changed files with 307 additions and 0 deletions

17
common/BuildProfile.ets Normal file
View File

@@ -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;
}

1
common/Index.ets Normal file
View File

@@ -0,0 +1 @@
export * from './src/main/ets/Index.ets';

View File

@@ -0,0 +1,10 @@
{
"apiType": "stageMode",
"buildOption": {
},
"targets": [
{
"name": "default"
}
]
}

6
common/hvigorfile.ts Normal file
View File

@@ -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. */
}

9
common/oh-package.json5 Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "common",
"version": "1.0.0",
"description": "基础设施层:网络 / 日志 / 状态 / 工具",
"main": "Index.ets",
"author": "",
"license": "Apache-2.0",
"dependencies": {}
}

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,18 @@
/**
* 统一响应包装:与后端约定 code / message / data 结构。
*/
export class ApiResult<T> {
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;
}
}

View File

@@ -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<string, string>): Record<string, string> {
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();
}
}
}

View File

@@ -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<string, string>): Promise<string> {
return this.request(url, http.RequestMethod.GET, undefined, header);
}
async post(url: string, body: string, header?: Record<string, string>): Promise<string> {
return this.request(url, http.RequestMethod.POST, body, header);
}
async put(url: string, body: string, header?: Record<string, string>): Promise<string> {
return this.request(url, http.RequestMethod.PUT, body, header);
}
async delete(url: string, header?: Record<string, string>): Promise<string> {
return this.request(url, http.RequestMethod.DELETE, undefined, header);
}
private async request(url: string, method: http.RequestMethod, body?: string,
header?: Record<string, string>): Promise<string> {
let headers: Record<string, string> = {};
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));
}
}
}

View File

@@ -0,0 +1,10 @@
/**
* 请求/响应拦截器接口。
* 实现类可注入请求头(鉴权)、统一处理响应(401/错误码)等。
*/
export interface HttpInterceptor {
/** 请求发出前,返回(可修改的)请求头。 */
onRequest(header: Record<string, string>): Record<string, string>;
/** 响应返回后,可据此抛错中断(如 401 触发登出)。 */
onResponse(responseCode: number, result: string): void;
}

View File

@@ -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;
}
}

View File

@@ -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}`;
}
}

View File

@@ -0,0 +1,10 @@
{
"module": {
"name": "common",
"type": "har",
"description": "$string:module_desc",
"deviceTypes": [
"phone"
]
}
}

View File

@@ -0,0 +1,8 @@
{
"string": [
{
"name": "module_desc",
"value": "common module"
}
]
}