新增教练端,实现业务闭环:会员登录注册→查询团课→预约团课→扫码签到→教练端开课→记录实际开课时间→教练端结课→记录实际结课时间→后台查询数据

This commit is contained in:
2026-07-20 17:21:28 +08:00
parent df0e68469b
commit 4a4697c816
84 changed files with 6914 additions and 258 deletions
+76
View File
@@ -0,0 +1,76 @@
const luchRequest = require('luch-request')
const Request = luchRequest.default || luchRequest
const { generateSignatureHeaders } = require('./signature')
const store = require('../store/index')
const BASE_URL = 'http://192.168.101.5:8084/api'
const http = new Request({
baseURL: BASE_URL,
timeout: 15000,
header: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
http.interceptors.request.use(
(config) => {
// 注入 JWT token
const token = store.getToken()
if (token) {
config.header = config.header || {}
config.header.Authorization = 'Bearer ' + token
}
// 注入签名头
const method = (config.method || 'GET').toUpperCase()
const url = config.url || ''
const body = config.data
const signatureHeaders = generateSignatureHeaders(method, url, body)
config.header = config.header || {}
Object.assign(config.header, signatureHeaders)
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
http.interceptors.response.use(
(response) => {
if (response.statusCode === 200) {
return response.data
}
return response
},
(error) => {
if (error.statusCode === 401) {
store.clearLogin()
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route !== 'pages/login/login') {
uni.reLaunch({ url: '/pages/login/login' })
}
}
return Promise.reject(error)
}
)
/**
* 将 coverImage 字段值解析为完整图片 URL
*/
function resolveCoverUrl(coverImage) {
if (!coverImage) return ''
if (/^\/api\/files\/\d+\/preview$/.test(coverImage)) {
return BASE_URL + coverImage.substring(4)
}
if (/^\d+$/.test(coverImage)) {
return BASE_URL + '/files/' + coverImage + '/preview'
}
return coverImage
}
module.exports = http
module.exports.resolveCoverUrl = resolveCoverUrl
@@ -0,0 +1,59 @@
const CryptoJS = require('crypto-js')
const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026'
/**
* 生成 HMAC-SHA256 签名
*/
function generateSignature(method, path, query, body, timestamp, nonce) {
const stringToSign = [method, path, query || '', body || '', String(timestamp), nonce].join('\n')
const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET)
return CryptoJS.enc.Base64.stringify(signature)
}
/**
* 生成随机 nonce
*/
function generateNonce() {
const timestamp = Date.now().toString(36)
const randomPart = Math.random().toString(36).substring(2, 15)
return timestamp + '-' + randomPart
}
/**
* 从 URL 解析 path 和 query
*/
function parseUrl(url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
const withoutProtocol = url.substring(url.indexOf('://') + 3)
const pathStart = withoutProtocol.indexOf('/')
if (pathStart === -1) return { path: '/', query: '' }
const pathAndQuery = withoutProtocol.substring(pathStart)
const queryIndex = pathAndQuery.indexOf('?')
if (queryIndex === -1) return { path: pathAndQuery, query: '' }
return { path: pathAndQuery.substring(0, queryIndex), query: pathAndQuery.substring(queryIndex + 1) }
}
const queryIndex = url.indexOf('?')
if (queryIndex === -1) return { path: url, query: '' }
return { path: url.substring(0, queryIndex), query: url.substring(queryIndex + 1) }
}
/**
* 生成签名请求头
*/
function generateSignatureHeaders(method, url, body) {
const timestamp = Date.now()
const nonce = generateNonce()
const { path, query } = parseUrl(url)
const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : ''
const signature = generateSignature(method.toUpperCase(), path, query, bodyStr, timestamp, nonce)
return {
'X-Signature': signature,
'X-Timestamp': String(timestamp),
'X-Nonce': nonce
}
}
module.exports = { generateSignatureHeaders }