feat(medications): 增加基于视觉AI的药品智能录入系统

构建了从照片到药品档案的自动化处理流程,通过GLM多模态大模型实现药品信息的智能采集:

核心能力:
- 创建任务追踪表 t_medication_recognition_tasks 存储识别任务状态
- 四阶段渐进式分析:基础识别→人群适配→成分解析→风险评估
- 提供三个REST端点支持任务创建、进度查询和结果确认
- 前端可通过轮询方式获取0-100%的实时进度反馈
- VIP用户免费使用,普通用户按次扣费

技术实现:
- 利用GLM-4V-Plus模型处理多角度药品图像(正面+侧面+说明书)
- 采用GLM-4-Flash模型进行文本深度分析
- 异步任务执行机制避免接口阻塞
- 完整的异常处理和任务失败恢复策略
- 新增AI_RECOGNITION.md文档详细说明集成方式

同步修复:
- 修正会员用户AI配额扣减逻辑,避免不必要的次数消耗
- 优化APNs推送中无效设备令牌的检测和清理流程
- 将服药提醒的提前通知时间从15分钟缩短为5分钟
This commit is contained in:
richarjiang
2025-11-21 10:27:59 +08:00
parent 75fbea2c90
commit a17fe0b965
14 changed files with 1706 additions and 74 deletions

View File

@@ -110,7 +110,8 @@ export class PushNotificationsService {
sentCount++;
} else {
const failure = apnsResults.failed[0];
const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`;
const error = failure.error as any;
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
await this.pushMessageService.updateMessageStatus(
message.id,
@@ -119,9 +120,12 @@ export class PushNotificationsService {
errorMessage
);
// 如果是无效令牌,停用该令牌
if (failure.status === '410' || failure.response?.reason === 'Unregistered') {
await this.pushTokenService.unregisterToken(userId, deviceToken);
// 检查是否是无效令牌错误 - 需要停用 token
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
if (shouldDeactivateToken) {
this.logger.warn(`Deactivating invalid token for user ${userId}: ${errorMessage}`);
await this.pushTokenService.deactivateToken(deviceToken);
}
results.push({
@@ -173,6 +177,40 @@ export class PushNotificationsService {
}
}
/**
* 判断是否应该停用 token
* 根据 APNs 错误类型判断 token 是否已失效
*/
private shouldDeactivateToken(error: any, response: any): boolean {
if (!error && !response) {
return false;
}
// 检查错误对象的 reason 字段apns2 库的错误格式)
const reason = error?.reason || response?.reason || '';
// APNs 返回的需要停用 token 的错误原因
const invalidTokenReasons = [
'BadDeviceToken', // 无效的设备令牌格式
'Unregistered', // 设备已注销(用户卸载了应用)
'DeviceTokenNotForTopic', // token 与 bundle ID 不匹配
'ExpiredToken', // token 已过期
];
// 检查是否包含这些错误原因
if (invalidTokenReasons.some(r => reason.includes(r))) {
return true;
}
// 检查 HTTP 状态码 410 (Gone) - 表示设备令牌永久失效
const statusCode = error?.statusCode || response?.statusCode;
if (statusCode === 410 || statusCode === '410') {
return true;
}
return false;
}
/**
* 使用模板发送推送通知
*/
@@ -311,7 +349,8 @@ export class PushNotificationsService {
} else {
// 发送失败
const failure = apnsResult as any;
const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`;
const error = failure.error as any;
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
await this.pushMessageService.updateMessageStatus(
message.id,
@@ -320,9 +359,12 @@ export class PushNotificationsService {
errorMessage
);
// 如果是无效令牌,停用该令牌
if (failure.status === '410' || failure.response?.reason === 'Unregistered') {
await this.pushTokenService.unregisterToken(userId, deviceToken);
// 检查是否是无效令牌错误 - 需要停用 token
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
if (shouldDeactivateToken) {
this.logger.warn(`Deactivating invalid token for user ${userId}: ${errorMessage}`);
await this.pushTokenService.deactivateToken(deviceToken);
}
results.push({
@@ -597,7 +639,8 @@ export class PushNotificationsService {
sentCount++;
} else {
const failure = apnsResults.failed[0];
const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`;
const error = failure.error as any;
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
await this.pushMessageService.updateMessageStatus(
message.id,
@@ -606,14 +649,12 @@ export class PushNotificationsService {
errorMessage
);
// 如果是无效令牌,停用该令牌
if (failure.status === '410' || failure.response?.reason === 'Unregistered') {
if (userId) {
await this.pushTokenService.unregisterToken(userId, deviceToken);
} else {
// 如果没有用户ID直接停用令牌
await this.pushTokenService.deactivateToken(deviceToken);
}
// 检查是否是无效令牌错误 - 需要停用 token
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
if (shouldDeactivateToken) {
this.logger.warn(`Deactivating invalid token for device ${deviceToken}: ${errorMessage}`);
await this.pushTokenService.deactivateToken(deviceToken);
}
results.push({
@@ -738,7 +779,8 @@ export class PushNotificationsService {
} else {
// 发送失败
const failure = apnsResult as any;
const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`;
const error = failure.error as any;
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
await this.pushMessageService.updateMessageStatus(
message.id,
@@ -747,14 +789,12 @@ export class PushNotificationsService {
errorMessage
);
// 如果是无效令牌,停用该令牌
if (failure.status === '410' || failure.response?.reason === 'Unregistered') {
if (userId) {
await this.pushTokenService.unregisterToken(userId, deviceToken);
} else {
// 如果没有用户ID直接停用令牌
await this.pushTokenService.deactivateToken(deviceToken);
}
// 检查是否是无效令牌错误 - 需要停用 token
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
if (shouldDeactivateToken) {
this.logger.warn(`Deactivating invalid token for device ${deviceToken}: ${errorMessage}`);
await this.pushTokenService.deactivateToken(deviceToken);
}
results.push({