2026-10-05 20:11:24 +08:00
|
|
|
import { ApiRequestError, getSession, requestJson } from '@/services/api.uts'
|
|
|
|
|
import { FEEDBACK_PENDING_KEY } from '@/services/cache-lifecycle.uts'
|
2026-10-01 20:26:41 +08:00
|
|
|
|
2026-10-05 20:11:24 +08:00
|
|
|
export type FeedbackItem = { id: string, content: string, status: string, reply: string, createdAt: string, handledAt: string, updatedAt: string }
|
2026-10-01 20:26:41 +08:00
|
|
|
export type FeedbackPage = { items: FeedbackItem[], page: number, total: number, hasMore: boolean }
|
2026-10-05 20:11:24 +08:00
|
|
|
export type PendingFeedback = { requestId: string, content: string }
|
|
|
|
|
|
|
|
|
|
const validId = (value: string): boolean => {
|
|
|
|
|
if (!/^[1-9][0-9]{0,9}$/.test(value)) return false
|
|
|
|
|
return !(value.length == 10 && value > '2147483647')
|
|
|
|
|
}
|
|
|
|
|
const validRequestId = (value: string): boolean => /^[A-Za-z0-9_-]{8,64}$/.test(value)
|
|
|
|
|
const validContent = (value: string): boolean => value.trim().length > 0 && value.length <= 255
|
|
|
|
|
const validStatuses = ['pending', 'processing', 'resolved', 'closed']
|
|
|
|
|
const requiredText = (data: UTSJSONObject, key: string, message: string): string => {
|
|
|
|
|
const value = data.getString(key)
|
|
|
|
|
if (value == null || value.length == 0) throw new Error(message)
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
const optionalText = (data: UTSJSONObject, key: string, message: string): string => {
|
|
|
|
|
const value = data[key]
|
|
|
|
|
if (value == null) return ''
|
|
|
|
|
if (typeof value != 'string') throw new Error(message)
|
|
|
|
|
return value as string
|
|
|
|
|
}
|
2026-10-01 20:26:41 +08:00
|
|
|
|
|
|
|
|
function feedbackView(data: UTSJSONObject): FeedbackItem {
|
|
|
|
|
const id = data.getString('id') ?? ''; const content = data.getString('content') ?? ''
|
2026-10-05 20:11:24 +08:00
|
|
|
const status = data.getString('status') ?? ''
|
|
|
|
|
if (!validId(id) || content.length == 0 || !validStatuses.includes(status)) throw new Error('反馈响应不完整')
|
|
|
|
|
const createdAt = requiredText(data, 'createdAt', '反馈时间格式无效')
|
|
|
|
|
// 后端对没有回复/处理时间的字段返回 null,统一转为空字符串供页面渲染。
|
|
|
|
|
const reply = optionalText(data, 'reply', '反馈回复格式无效')
|
|
|
|
|
const handledAt = optionalText(data, 'handledAt', '反馈处理时间格式无效')
|
|
|
|
|
const updatedAt = requiredText(data, 'updatedAt', '反馈更新时间格式无效')
|
|
|
|
|
return { id, content, status, reply,
|
|
|
|
|
createdAt, handledAt, updatedAt }
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function feedbackKey(): string { return 'feedback_' + Date.now().toString() + '_' + Math.floor(Math.random() * 1000000000).toString() }
|
2026-10-05 20:11:24 +08:00
|
|
|
|
|
|
|
|
function pendingStorageKey(userId: string): string {
|
|
|
|
|
if (!validId(userId) || getSession()?.getString('userId') != userId) throw new Error('请重新登录原账户后确认反馈')
|
|
|
|
|
return 'airz:v1:user:' + userId + ':' + FEEDBACK_PENDING_KEY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 只恢复已发送命令,不缓存普通草稿、Token、反馈列表或回复。坏存储不能当作空记录覆盖。 */
|
|
|
|
|
export function loadPendingFeedback(userId: string): PendingFeedback | null {
|
|
|
|
|
const key = pendingStorageKey(userId)
|
|
|
|
|
const exists = uni.getStorageInfoSync().keys.includes(key)
|
|
|
|
|
const value = uni.getStorageSync(key)
|
|
|
|
|
// H5同步读取失败也可能返回空值,已有键不能按“没有提交”处理。
|
|
|
|
|
if (value == null || value === '') {
|
|
|
|
|
if (exists || uni.getStorageInfoSync().keys.includes(key)) throw new Error('本机待确认反馈读取失败,请重新读取提交状态')
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
if (typeof value != 'object' || Array.isArray(value)) throw new Error('本机待确认反馈读取失败,请重新读取提交状态')
|
|
|
|
|
const data = value as UTSJSONObject
|
|
|
|
|
const requestId = data.getString('requestId') ?? ''; const content = data.getString('content') ?? ''
|
|
|
|
|
if (UTSJSONObject.keys(data).length != 2 || !validRequestId(requestId) || !validContent(content) || content != content.trim())
|
|
|
|
|
throw new Error('本机待确认反馈不完整,请联系客服核对')
|
|
|
|
|
return { requestId, content }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 发请求前保存并回读;已有未知命令始终沿用,不因重新输入而生成新键。 */
|
|
|
|
|
export function prepareFeedback(userId: string, content: string): PendingFeedback {
|
|
|
|
|
const previous = loadPendingFeedback(userId)
|
|
|
|
|
if (previous != null) return previous
|
|
|
|
|
const value = content.trim()
|
|
|
|
|
if (!validContent(value)) throw new Error('请填写1至255字的反馈内容')
|
|
|
|
|
const pending: PendingFeedback = { requestId: feedbackKey(), content: value }
|
|
|
|
|
uni.setStorageSync(pendingStorageKey(userId), pending)
|
|
|
|
|
const saved = loadPendingFeedback(userId)
|
|
|
|
|
if (saved == null || saved.requestId != pending.requestId || saved.content != pending.content)
|
|
|
|
|
throw new Error('本机未能保存提交状态,请稍后重试')
|
|
|
|
|
return saved
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 比较原命令后才移除,迟到回执不能清除另一条待确认提交。 */
|
|
|
|
|
export function clearPendingFeedback(userId: string, pending: PendingFeedback): void {
|
|
|
|
|
const saved = loadPendingFeedback(userId)
|
|
|
|
|
if (saved == null || saved.requestId != pending.requestId || saved.content != pending.content)
|
|
|
|
|
throw new Error('本机提交状态已变化,请重新读取提交状态')
|
|
|
|
|
const key = pendingStorageKey(userId)
|
|
|
|
|
uni.removeStorageSync(key)
|
|
|
|
|
if (uni.getStorageInfoSync().keys.includes(key)) throw new Error('反馈已确认,但本机状态未清理,请再次确认')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function feedbackRejected(error: any | null): boolean {
|
|
|
|
|
// 参数在写入前确定被拒绝;冲突代表已有记录,401/网络/服务故障与坏回执仍保留原键。
|
|
|
|
|
return error instanceof ApiRequestError && (error.status == 400 || error.status == 413 || error.status == 422)
|
|
|
|
|
&& ['VALIDATION_ERROR', 'FEEDBACK_ATTACHMENTS_UNAVAILABLE'].includes(error.errorCode)
|
|
|
|
|
}
|
2026-10-01 20:26:41 +08:00
|
|
|
export function feedbackStatus(status: string): string {
|
|
|
|
|
if (status == 'pending') return '待处理'
|
|
|
|
|
if (status == 'processing') return '处理中'
|
|
|
|
|
if (status == 'resolved') return '已回复'
|
|
|
|
|
if (status == 'closed') return '已关闭'
|
|
|
|
|
return '待核对'
|
|
|
|
|
}
|
|
|
|
|
export async function submitFeedback(content: string, requestId: string): Promise<FeedbackItem> {
|
2026-10-05 20:11:24 +08:00
|
|
|
const value = content.trim()
|
|
|
|
|
if (value.length == 0) throw new Error('请输入反馈内容')
|
|
|
|
|
if (value.length > 255) throw new Error('反馈内容不能超过255字')
|
|
|
|
|
if (!validRequestId(requestId)) throw new Error('反馈请求标识无效')
|
2026-10-01 20:26:41 +08:00
|
|
|
const attachmentIds: string[] = []
|
2026-10-05 20:11:24 +08:00
|
|
|
const receipt = await requestJson('/feedback', 'POST', { content: value, requestId, attachmentIds })
|
|
|
|
|
if (receipt.getString('requestId') != requestId) throw new Error('提交回执未能核对,请确认上次提交')
|
|
|
|
|
return feedbackView(receipt)
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|
2026-10-05 20:11:24 +08:00
|
|
|
export async function fetchFeedback(page: number, pageSize: number = 20): Promise<FeedbackPage> {
|
|
|
|
|
if (!Number.isInteger(page) || page < 1 || page > 1000 || !Number.isInteger(pageSize) || pageSize < 1 || pageSize > 50) throw new Error('反馈分页参数无效')
|
|
|
|
|
const data = await requestJson('/feedback', 'GET', { page: page.toString(), pageSize: pageSize.toString() })
|
|
|
|
|
const rowsValue = data['items']
|
|
|
|
|
if (rowsValue == null || !Array.isArray(rowsValue)) throw new Error('反馈记录加载失败')
|
|
|
|
|
const rows = rowsValue as UTSJSONObject[]
|
|
|
|
|
rows.forEach((item: UTSJSONObject) => {
|
|
|
|
|
if (item == null || typeof item != 'object' || Array.isArray(item)) throw new Error('反馈记录格式无效')
|
|
|
|
|
})
|
|
|
|
|
const returnedPage = data.getNumber('page') ?? page; const total = data.getNumber('total') ?? 0
|
|
|
|
|
if (!Number.isInteger(returnedPage) || returnedPage < 1 || !Number.isInteger(total) || total < 0) throw new Error('反馈分页响应无效')
|
|
|
|
|
const hasMore = data.getBoolean('hasMore')
|
|
|
|
|
if (hasMore == null) throw new Error('反馈分页标记无效')
|
|
|
|
|
return { items: rows.map((item: UTSJSONObject): FeedbackItem => feedbackView(item)), page: returnedPage,
|
|
|
|
|
total, hasMore }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 读取单条反馈的最新处理状态,严格由后端按当前会员校验归属。 */
|
|
|
|
|
export async function fetchFeedbackDetail(id: string): Promise<FeedbackItem> {
|
|
|
|
|
if (!validId(id)) throw new Error('反馈编号无效')
|
|
|
|
|
const item = feedbackView(await requestJson('/feedback/' + encodeURIComponent(id), 'GET', {}))
|
|
|
|
|
if (item.id != id) throw new Error('反馈详情编号不一致')
|
|
|
|
|
return item
|
2026-10-01 20:26:41 +08:00
|
|
|
}
|