dqm-api-client
Version:
A Node.js client library for court booking API services including authentication, SMS verification, and court management
275 lines (249 loc) • 7.4 kB
JavaScript
const { encodeOpenId, getOrderParams } = require('../utils/common');
const { get, post, put } = require('../utils/request');
const AppError = require('../errors/AppError');
const { ERROR_CODES } = require('../constants/errorCodes');
class ApiService {
#baseURL;
#openid;
#org;
#token;
#memberId;
#init;
constructor(openid) {
this.#baseURL = 'https://fdsaas.hulasports.com/api/';
this.#openid = openid;
this.#org = '67623479acb2a720bd8e21bc'; // 默认或初始组织ID
// 实例状态
this.#token = '';
this.#memberId = '';
this.#init = false; // 标记是否已认证
}
/**
* 动态生成请求头,确保信息总是最新的
* @private
*/
_getHeaders() {
const headers = {
openid: encodeOpenId(this.#openid),
appId: 'wx85217acaed0bb9af',
_org: this.#org, // 总是包含组织ID
};
// 只有在获取到 token 和 memberId 后才添加到请求头
if (this.#token) {
headers.token = this.#token;
}
if (this.#memberId) {
headers.memberId = this.#memberId;
}
return headers;
}
/**
* 统一处理认证成功后的状态更新
* @private
* @param {object} authData - 包含 member 信息的响应数据
*/
_setAuthenticated(authData) {
if (authData && authData.member) {
this.#memberId = authData.member._id;
this.#token = authData.member.token;
// 如果API返回了新的组织ID,则更新
if (authData.member._organization) {
this.#org = authData.member._organization;
}
this.#init = true;
console.log('认证成功,状态已更新。');
} else {
console.error('认证数据格式不正确', authData);
throw new AppError('Invalid authentication data format', {
name: 'AuthenticationDataError',
code: ERROR_CODES.INVALID_AUTH_DATA,
data: { authData },
isOperational: false
});
}
}
/**
* 在需要认证的API调用前执行此检查
* @private
*/
async _ensureAuthenticated() {
if (!this.#init) {
try {
await this.getMemberInfo();
} catch (error) {
console.error('获取会员信息时发生错误:', error);
throw new AppError('Failed to authenticate user', {
name: 'AuthenticationError',
code: ERROR_CODES.AUTH_FAILED,
data: { originalError: error.message },
isOperational: true
});
}
}
}
/**
* 使用 openid 获取会员信息并完成登录
*/
async getMemberInfo() {
const url = `${this.#baseURL}members/wx/${this.#openid}?_org=${this.#org}&state=xiaochengxu`;
const response = await get(url, this._getHeaders());
const { code, data } = response.data;
if (code === 200) {
this._setAuthenticated(data);
return data;
} else {
throw new AppError('Failed to get member info', {
name: 'MemberInfoError',
code: ERROR_CODES.MEMBER_INFO_FAILED,
data: { code, responseData: data },
isOperational: true
});
}
}
/**
* 使用手机号和验证码登录/绑定手机
* @param {string} tel - 手机号
* @param {string} smsCode - 短信验证码
* @returns {Object} 登录成功后的会员信息
*/
async login(tel, smsCode) {
const url = this.#baseURL + 'members/tel/bind';
const body = {
state: 'xiaochengxu',
tel: tel,
code: smsCode,
_org: this.#org,
wxid: this.#openid,
};
console.log('登录参数:', body);
// login 方法本身不需要 token,所以直接使用当前生成的 headers
const response = await put(url, body, this._getHeaders());
const { code, data } = response.data;
if (code === 200) {
console.log('登录成功:', data);
this._setAuthenticated(data);
return data;
} else {
console.log('登录失败响应:', response.data);
throw new AppError('Login failed', {
name: 'LoginError',
code: ERROR_CODES.LOGIN_FAILED,
data: { code, responseData: data, tel },
isOperational: true
});
}
}
/**
* 发送验证码(无需登录)
* @param {string} tel - 手机号
* @param {string} action - 'login' 或其他
*/
async sendSmsCode(tel, action) {
const url = this.#baseURL + 'smscode';
let body = {
tel: tel,
_org: this.#org, // 使用初始的组织ID
};
if (action === 'login') {
body.state = 'login';
} else {
body._venue = '676a26539f1ee424e50fa0af'; // 根据需要添加
}
// 此方法不需要token或memberId
const response = await post(url, body, this._getHeaders());
const { code, data } = response.data;
if (code !== 200) {
throw new AppError('Failed to send SMS code', {
name: 'SmsCodeError',
code: ERROR_CODES.SMS_CODE_FAILED,
data: { code, responseData: data, tel, action },
isOperational: true
});
}
console.log('发送验证码成功:', data);
return data;
}
/**
* 根据日期查询场地(需要登录)
* @param {number} orderDateNum
*/
async getCourtByDate(orderDateNum) {
await this._ensureAuthenticated(); // 检查是否已登录
const url = this.#baseURL + 'orderlists/get/book';
const body = {
_org: this.#org,
_member: this.#memberId,
orderDateNum: orderDateNum,
_venue: '676a26539f1ee424e50fa0af',
_item: '676a24637fa48934627cb2f6',
passBaseOn: 'start',
showLine: 'row',
showPassTime: false,
delayMins: 0,
};
// console.log('查询场地参数:', body);
const response = await post(url, body, this._getHeaders());
const { code, data } = response.data;
if (code !== 200) {
throw new AppError('Failed to query court data', {
name: 'CourtQueryError',
code: ERROR_CODES.COURT_QUERY_FAILED,
data: { code, responseData: data, orderDateNum },
isOperational: true
});
}
return data;
}
/**
* 创建订单(需要登录)
* @param {object} body
*/
async createOrder(orderDate, orderArr, smscode = '') {
await this._ensureAuthenticated(); // 检查是否已登录
const orderParam = getOrderParams(
this.#org,
this.#memberId,
orderDate,
orderArr,
this.#openid,
smscode
);
const url = this.#baseURL + 'wxpay/order';
const response = await post(url, orderParam, this._getHeaders());
const { code, data } = response.data;
if (code !== 200) {
throw new AppError('Failed to create order', {
name: 'OrderCreationError',
code: ERROR_CODES.ORDER_CREATION_FAILED,
data: { code, responseData: data, orderDate, orderArrLength: orderArr.length },
isOperational: true
});
}
console.log('创建订单成功:', data);
return data;
}
// Getter 方法 - 提供对私有变量的受控访问
get baseURL() {
return this.#baseURL;
}
get org() {
return this.#org;
}
get token() {
return this.#token;
}
get memberId() {
return this.#memberId;
}
get openid() {
return this.#openid;
}
get isAuthenticated() {
return this.#init;
}
// Setter 方法 - 允许外部更新组织ID(如果需要)
set org(newOrg) {
this.#org = newOrg;
}
}
module.exports = ApiService;