feat: 修复 Redis 超时问题、固定小程序端口、新增人生事件模块及优化多个页面
- 修复 Redis 超时:添加 commons-pool2 依赖,启用 Lettuce 连接池,超时提升至 15s - 固定 mini-program H5 端口为 5175,避免与 web 项目端口冲突 - 新增人生事件(life-event)模块:表单和详情页面 - 新增 EpicScript 灵感接口(Controller/Service/DTO) - 优化登录、引导、主页、记录、剧本详情等多个页面 - 优化服务管理脚本和 Nginx 配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,11 @@ export const validateToken = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const validateTokenStrict = async () => {
|
||||
const response = await get('/auth/validateToken')
|
||||
return response.data === true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {Promise<Object>} 用户信息
|
||||
@@ -118,6 +123,7 @@ export default {
|
||||
logout,
|
||||
refreshToken,
|
||||
validateToken,
|
||||
validateTokenStrict,
|
||||
getCurrentUserInfo,
|
||||
checkPhone
|
||||
}
|
||||
|
||||
@@ -1,139 +1,117 @@
|
||||
/**
|
||||
* 爽文剧本服务
|
||||
* 处理剧本的增删改查
|
||||
*/
|
||||
|
||||
import { get, post, put, del } from './request.js'
|
||||
|
||||
export const getScriptList = async () => {
|
||||
const response = await get('/epicScript/listAll')
|
||||
return response
|
||||
return get('/epicScript/listAll')
|
||||
}
|
||||
|
||||
export const getScriptPage = async ({ pageNum = 1, pageSize = 10 }) => {
|
||||
const response = await get('/epicScript/page', { pageNum, pageSize })
|
||||
return response
|
||||
return get('/epicScript/page', { pageNum, pageSize })
|
||||
}
|
||||
|
||||
export const getScriptById = async (id) => {
|
||||
const response = await get('/epicScript/detail', { id })
|
||||
return response
|
||||
return get('/epicScript/detail', { id })
|
||||
}
|
||||
|
||||
export const getInspirationRecommendations = async () => {
|
||||
return get('/epicScript/inspiration/recommendations')
|
||||
}
|
||||
|
||||
export const getRandomInspirations = async (size = 3) => {
|
||||
return get('/epicScript/inspiration/random', { size })
|
||||
}
|
||||
|
||||
export const generateFromInspiration = async (payload) => {
|
||||
return post('/epicScript/inspiration/generate', payload)
|
||||
}
|
||||
|
||||
export const createScript = async (scriptData) => {
|
||||
const requestData = transformToBackendFormat(scriptData)
|
||||
const response = await post('/epicScript/create', requestData)
|
||||
return response
|
||||
return post('/epicScript/create', transformToBackendFormat(scriptData))
|
||||
}
|
||||
|
||||
export const updateScript = async (scriptData) => {
|
||||
const requestData = transformToBackendFormat(scriptData)
|
||||
const response = await put('/epicScript/update', requestData)
|
||||
return response
|
||||
return put('/epicScript/update', transformToBackendFormat(scriptData))
|
||||
}
|
||||
|
||||
export const selectScript = async (id) => {
|
||||
const response = await put('/epicScript/select', { id })
|
||||
return response
|
||||
return put('/epicScript/select', { id })
|
||||
}
|
||||
|
||||
export const deleteScript = async (id) => {
|
||||
const response = await del('/epicScript/delete', { id })
|
||||
return response
|
||||
return del('/epicScript/delete', { id })
|
||||
}
|
||||
|
||||
const textOrFallback = (value, fallback) => {
|
||||
return value && String(value).trim() ? String(value).trim() : fallback
|
||||
}
|
||||
|
||||
export const buildCharacterInfo = (profile = {}) => {
|
||||
const hobbies = Array.isArray(profile.hobbies) && profile.hobbies.length
|
||||
? profile.hobbies.join('、')
|
||||
: '未设置'
|
||||
|
||||
const parts = [
|
||||
`昵称:${textOrFallback(profile.nickname, '未设置')}`,
|
||||
`性别:${textOrFallback(profile.gender, '未设置')}`,
|
||||
`MBTI:${textOrFallback(profile.mbti, '未设置')}`,
|
||||
`星座:${textOrFallback(profile.zodiac, '未设置')}`,
|
||||
`职业:${textOrFallback(profile.profession, '未设置')}`,
|
||||
`兴趣:${hobbies}`
|
||||
]
|
||||
|
||||
if (profile.future?.vision) parts.push(`未来愿景:${profile.future.vision}`)
|
||||
if (profile.future?.ideal) parts.push(`理想生活:${profile.future.ideal}`)
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
export const buildLifeEventsSummary = (events = [], profile = {}) => {
|
||||
const parts = []
|
||||
const addMemory = (label, memory) => {
|
||||
if (memory?.text) {
|
||||
parts.push(`【${label}】${memory.date || '未知日期'}:${memory.text}`)
|
||||
}
|
||||
}
|
||||
|
||||
addMemory('童年记忆', profile.childhood)
|
||||
addMemory('高光时刻', profile.joy)
|
||||
addMemory('低谷经历', profile.low)
|
||||
|
||||
events.forEach((event) => {
|
||||
const tag = Array.isArray(event.tags) && event.tags.length ? ` #${event.tags.join(' #')}` : ''
|
||||
parts.push(`【人生事件】${event.time || event.date || '未知日期'} ${event.title || '未命名'}${tag}:${event.content || event.description || ''}`)
|
||||
})
|
||||
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
const transformToBackendFormat = (frontendData) => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
content,
|
||||
isSelected,
|
||||
character,
|
||||
events
|
||||
events,
|
||||
plotJson
|
||||
} = frontendData
|
||||
|
||||
let title = theme || '我的剧本'
|
||||
let plotIntro = ''
|
||||
let plotTurning = ''
|
||||
let plotClimax = ''
|
||||
let plotEnding = ''
|
||||
|
||||
if (content) {
|
||||
const sections = content.split(/【[^】]+】/)
|
||||
const titles = content.match(/【[^】]+】/g) || []
|
||||
|
||||
titles.forEach((t, index) => {
|
||||
const sectionContent = sections[index + 1]?.trim() || ''
|
||||
if (t.includes('序幕') || t.includes('低谷')) {
|
||||
plotIntro = sectionContent
|
||||
} else if (t.includes('转折') || t.includes('契机')) {
|
||||
plotTurning = sectionContent
|
||||
} else if (t.includes('高潮') || t.includes('抉择')) {
|
||||
plotClimax = sectionContent
|
||||
} else if (t.includes('结局') || t.includes('开始')) {
|
||||
plotEnding = sectionContent
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let characterInfo = ''
|
||||
if (character) {
|
||||
const parts = [
|
||||
`姓名:${character.nickname || '未设置'}`,
|
||||
`性别:${character.gender || '未设置'}`,
|
||||
`MBTI:${character.mbti || '未设置'}`,
|
||||
`星座:${character.zodiac || '未设置'}`,
|
||||
`职业:${character.profession || '未设置'}`,
|
||||
`兴趣爱好:${character.hobbies?.join(',') || '无'}`
|
||||
]
|
||||
|
||||
if (character.future) {
|
||||
if (character.future.vision) parts.push(`未来愿景:${character.future.vision}`)
|
||||
if (character.future.ideal) parts.push(`理想生活:${character.future.ideal}`)
|
||||
}
|
||||
|
||||
characterInfo = parts.join('\n')
|
||||
}
|
||||
|
||||
let lifeEventsSummary = ''
|
||||
const eventParts = []
|
||||
|
||||
if (character) {
|
||||
if (character.childhood?.text) {
|
||||
eventParts.push(`【童年记忆】(${character.childhood.date || '未知时间'}): ${character.childhood.text}`)
|
||||
}
|
||||
if (character.joy?.text) {
|
||||
eventParts.push(`【高光时刻】(${character.joy.date || '未知时间'}): ${character.joy.text}`)
|
||||
}
|
||||
if (character.low?.text) {
|
||||
eventParts.push(`【至暗时刻】(${character.low.date || '未知时间'}): ${character.low.text}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (events && Array.isArray(events)) {
|
||||
events.forEach(e => {
|
||||
const dateStr = e.time || e.eventDate || '未知时间'
|
||||
const titleStr = e.title || '无标题'
|
||||
const contentStr = e.content || ''
|
||||
eventParts.push(`【人生事件】(${dateStr}) ${titleStr}${contentStr ? ': ' + contentStr : ''}`)
|
||||
})
|
||||
}
|
||||
|
||||
lifeEventsSummary = eventParts.join('\n')
|
||||
const scriptTitle = title || theme || '我的人生剧本'
|
||||
const characterInfo = frontendData.characterInfo || buildCharacterInfo(character || {})
|
||||
const lifeEventsSummary = frontendData.lifeEventsSummary || buildLifeEventsSummary(events || [], character || {})
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
title: scriptTitle,
|
||||
theme,
|
||||
style,
|
||||
length,
|
||||
plotIntro,
|
||||
plotTurning,
|
||||
plotClimax,
|
||||
plotEnding,
|
||||
plotJson: content ? { fullContent: content } : null,
|
||||
plotIntro: frontendData.plotIntro || '',
|
||||
plotTurning: frontendData.plotTurning || '',
|
||||
plotClimax: frontendData.plotClimax || '',
|
||||
plotEnding: frontendData.plotEnding || '',
|
||||
plotJson: plotJson || (content ? { fullContent: content } : null),
|
||||
isSelected,
|
||||
characterInfo,
|
||||
lifeEventsSummary
|
||||
@@ -156,7 +134,8 @@ export const transformToFrontendFormat = (backendData) => {
|
||||
plotEnding,
|
||||
plotJson,
|
||||
isSelected,
|
||||
createTime
|
||||
createTime,
|
||||
updateTime
|
||||
} = backendData
|
||||
|
||||
let content = ''
|
||||
@@ -171,6 +150,9 @@ export const transformToFrontendFormat = (backendData) => {
|
||||
content = parts.join('\n\n')
|
||||
}
|
||||
|
||||
const plainContent = content.replace(/[#>*_`-]/g, '').trim()
|
||||
const summary = plainContent ? plainContent.slice(0, 90) : (theme || '一段正在生成的人生爽文。')
|
||||
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
@@ -179,8 +161,15 @@ export const transformToFrontendFormat = (backendData) => {
|
||||
style: style || '',
|
||||
length: length || 'medium',
|
||||
content,
|
||||
isSelected: isSelected || false,
|
||||
date: createTime ? new Date(createTime).toLocaleDateString() : new Date().toLocaleDateString()
|
||||
summary,
|
||||
plotJson,
|
||||
isSelected: Boolean(isSelected),
|
||||
createTime,
|
||||
updateTime,
|
||||
date: createTime ? String(createTime).slice(0, 10) : new Date().toLocaleDateString(),
|
||||
mode: plotJson?.mode || 'custom',
|
||||
prompt: plotJson?.prompt || '',
|
||||
wordCount: content ? content.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,10 +182,15 @@ export default {
|
||||
getScriptList,
|
||||
getScriptPage,
|
||||
getScriptById,
|
||||
getInspirationRecommendations,
|
||||
getRandomInspirations,
|
||||
generateFromInspiration,
|
||||
createScript,
|
||||
updateScript,
|
||||
selectScript,
|
||||
deleteScript,
|
||||
buildCharacterInfo,
|
||||
buildLifeEventsSummary,
|
||||
transformToFrontendFormat,
|
||||
transformListToFrontend
|
||||
}
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
import { getEnvValue, isDev } from '../config/env.js'
|
||||
import { getEnvValue, getConfig } from '../config/env.js'
|
||||
|
||||
const API_BASE_URL = getEnvValue('API_BASE_URL')
|
||||
|
||||
/**
|
||||
* 请求拦截处理
|
||||
* 自动添加 token 到请求头
|
||||
*/
|
||||
const AUTH_PATH_PREFIX = '/auth/'
|
||||
|
||||
const isAuthPath = (path = '') => path.startsWith(AUTH_PATH_PREFIX)
|
||||
|
||||
const createRequestError = (message, meta = {}) => {
|
||||
const error = new Error(message || 'Request failed')
|
||||
Object.assign(error, meta)
|
||||
return error
|
||||
}
|
||||
|
||||
const clearAuthStorage = () => {
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.removeStorageSync('refresh_token')
|
||||
}
|
||||
|
||||
const logApi = (level, label, payload, force = false) => {
|
||||
const debug = getEnvValue('DEBUG')
|
||||
if (!debug && !force) return
|
||||
const logger = level === 'error' ? console.error : console.log
|
||||
logger(`[API] ${label}`, payload)
|
||||
}
|
||||
|
||||
export const logRuntimeEnv = (source = 'startup') => {
|
||||
const config = getConfig()
|
||||
console.log('[ENV] runtime', {
|
||||
source,
|
||||
rawEnv: import.meta.env.VITE_APP_ENV,
|
||||
mode: import.meta.env.MODE,
|
||||
apiBaseUrl: config.API_BASE_URL,
|
||||
wsUrl: config.WS_URL,
|
||||
debug: config.DEBUG,
|
||||
platform: typeof uni !== 'undefined' ? uni.getSystemInfoSync?.()?.platform : 'unknown'
|
||||
})
|
||||
}
|
||||
|
||||
const getHeaders = () => {
|
||||
const token = uni.getStorageSync('access_token')
|
||||
const headers = {
|
||||
@@ -17,81 +48,82 @@ const getHeaders = () => {
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一请求方法
|
||||
* @param {Object} options - 请求配置
|
||||
* @returns {Promise} 请求 Promise
|
||||
*/
|
||||
const request = (options) => {
|
||||
const debug = getEnvValue('DEBUG')
|
||||
const method = options.method || 'GET'
|
||||
const fullUrl = `${API_BASE_URL}${options.url}`
|
||||
const forceLog = isAuthPath(options.url)
|
||||
|
||||
// 请求日志
|
||||
if (debug) {
|
||||
console.log('[API Request]', {
|
||||
method: options.method || 'GET',
|
||||
url: fullUrl,
|
||||
path: options.url,
|
||||
env: import.meta.env.VITE_APP_ENV
|
||||
})
|
||||
}
|
||||
logApi('log', 'request', {
|
||||
method,
|
||||
url: fullUrl,
|
||||
path: options.url,
|
||||
env: import.meta.env.VITE_APP_ENV || import.meta.env.MODE
|
||||
}, forceLog)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: fullUrl,
|
||||
method: options.method || 'GET',
|
||||
method,
|
||||
data: options.data,
|
||||
header: getHeaders(),
|
||||
timeout: 30000,
|
||||
success: (res) => {
|
||||
const { data, statusCode } = res
|
||||
const success = statusCode >= 200 && statusCode < 300 && (data?.code === 200 || data?.code === 0)
|
||||
|
||||
// 响应日志
|
||||
if (debug) {
|
||||
console.log('[API Response]', {
|
||||
path: options.url,
|
||||
status: statusCode,
|
||||
code: data?.code,
|
||||
success: data?.code === 200 || data?.code === 0
|
||||
})
|
||||
}
|
||||
logApi('log', 'response', {
|
||||
path: options.url,
|
||||
status: statusCode,
|
||||
code: data?.code,
|
||||
success
|
||||
}, forceLog || statusCode >= 400)
|
||||
|
||||
// 处理 401 未授权
|
||||
if (statusCode === 401) {
|
||||
uni.removeStorageSync('access_token')
|
||||
uni.removeStorageSync('refresh_token')
|
||||
uni.redirectTo({ url: '/pages/login/index' })
|
||||
reject(new Error('登录已过期,请重新登录'))
|
||||
if (!forceLog) {
|
||||
clearAuthStorage()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
reject(createRequestError('Login expired, please login again', {
|
||||
statusCode,
|
||||
code: data?.code,
|
||||
path: options.url,
|
||||
isAuthRejected: true
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// 后端返回格式:{ code, message, data }
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
if (success) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.message || '请求失败'))
|
||||
return
|
||||
}
|
||||
|
||||
reject(createRequestError(data?.message || 'Request failed', {
|
||||
statusCode,
|
||||
code: data?.code,
|
||||
path: options.url,
|
||||
response: data,
|
||||
isAuthRejected: statusCode === 403 || data?.code === 401 || data?.code === 403
|
||||
}))
|
||||
},
|
||||
fail: (err) => {
|
||||
// 错误日志
|
||||
if (debug) {
|
||||
console.error('[API Error]', {
|
||||
path: options.url,
|
||||
error: err.errMsg,
|
||||
env: import.meta.env.VITE_APP_ENV
|
||||
})
|
||||
}
|
||||
reject(new Error(err.errMsg || '网络错误'))
|
||||
logApi('error', 'fail', {
|
||||
path: options.url,
|
||||
url: fullUrl,
|
||||
error: err.errMsg,
|
||||
env: import.meta.env.VITE_APP_ENV || import.meta.env.MODE
|
||||
}, true)
|
||||
|
||||
reject(createRequestError(err.errMsg || 'Network request failed', {
|
||||
path: options.url,
|
||||
isNetworkError: true,
|
||||
originalError: err
|
||||
}))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
export const get = (url, params = {}) => {
|
||||
// 构建查询字符串
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
@@ -100,23 +132,14 @@ export const get = (url, params = {}) => {
|
||||
return request({ url: fullUrl, method: 'GET' })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
export const post = (url, data = {}) => {
|
||||
return request({ url, method: 'POST', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
export const put = (url, data = {}) => {
|
||||
return request({ url, method: 'PUT', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
*/
|
||||
export const del = (url, params = {}) => {
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
||||
@@ -130,5 +153,6 @@ export default {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del
|
||||
del,
|
||||
logRuntimeEnv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user