Ver código fonte

feat:用户提交的图片做OCR识别

master
leiyun 2 dias atrás
pai
commit
cc33dba4df
6 arquivos alterados com 150 adições e 8 exclusões
  1. +15
    -0
      sql/sys_config.sql
  2. +14
    -0
      src/controller/common.js
  3. +24
    -2
      src/controller/mp.js
  4. +30
    -0
      src/model/sys_config.js
  5. +59
    -0
      src/service/ocr.js
  6. +8
    -6
      view/admin/patient_index.html

+ 15
- 0
sql/sys_config.sql Ver arquivo

@@ -0,0 +1,15 @@
-- 系统配置表
CREATE TABLE IF NOT EXISTS `sys_config` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`config_key` varchar(50) NOT NULL COMMENT '配置标识',
`config_value` text COMMENT '配置值(JSON)',
`remark` varchar(200) DEFAULT '' COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';

-- 初始化瘤种选项
INSERT INTO `sys_config` (`config_key`, `config_value`, `remark`) VALUES
('tag_options', '["直肠癌","结肠癌","十二指肠癌","空肠癌","回肠癌","胃肠道间质瘤(GIST)","肠道神经内分泌肿瘤","腺瘤性息肉","炎性息肉","增生性息肉","平滑肌瘤","脂肪瘤"]', '瘤种选项列表');

+ 14
- 0
src/controller/common.js Ver arquivo

@@ -135,6 +135,20 @@ module.exports = class extends think.Controller {
};
}

/**
* 获取瘤种选项列表
* GET /common/tagOptions
*/
async tagOptionsAction() {
try {
const tags = await this.model('sys_config').getByKey('tag_options');
return this.json({ code: 0, data: tags || [] });
} catch (error) {
think.logger.error('获取瘤种选项失败:', error);
return this.json({ code: 0, data: [] });
}
}

/**
* 获取省市区树形数据(一次返回)
* GET /common/regions


+ 24
- 2
src/controller/mp.js Ver arquivo

@@ -281,7 +281,7 @@ module.exports = class extends Base {
async saveMyInfoAction() {
const mpUser = this.mpUser;
if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
const { gender, province_code, city_code, district_code, address, emergency_contact, emergency_phone, tag, documents, sign_income, sign_privacy, sign_promise, income_amount, mp_env_version } = this.post();
const { gender, province_code, city_code, district_code, address, emergency_contact, emergency_phone, documents, sign_income, sign_privacy, sign_promise, income_amount, mp_env_version } = this.post();
const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '请先完成实名认证' });
if (!province_code || !city_code || !district_code) return this.json({ code: 1, msg: '请选择省市区' });
@@ -297,7 +297,7 @@ module.exports = class extends Base {
gender: gender || '', province_code: province_code || '', city_code: city_code || '',
district_code: district_code || '', address: address || '',
emergency_contact: emergency_contact || '', emergency_phone: emergency_phone || '',
tag: tag || '', documents: JSON.stringify(documents || []),
documents: JSON.stringify(documents || []),
sign_income: sign_income || '',
sign_privacy: sign_privacy || '',
sign_promise: sign_promise || '',
@@ -312,6 +312,28 @@ module.exports = class extends Base {
operator_id: 0,
operator_name: '用户自助提交'
});

// 异步 OCR 识别瘤种(不阻塞用户提交)
const patientId = user.patient_id;
const docUrls = documents || [];
if (docUrls.length) {
setImmediate(async () => {
try {
const tagOptions = await think.model('sys_config').getByKey('tag_options');
if (tagOptions && tagOptions.length) {
const ocrService = think.service('ocr');
const matchedTag = await ocrService.matchTag(docUrls, tagOptions);
if (matchedTag) {
await think.model('patient').where({ id: patientId }).update({ tag: matchedTag });
think.logger.info(`[OCR] 患者(ID:${patientId}) 识别瘤种: ${matchedTag}`);
}
}
} catch (err) {
think.logger.error(`[OCR] 患者(ID:${patientId}) 识别失败:`, err.message);
}
});
}

return this.json({ code: 0, data: {}, msg: '提交成功' });
} catch (error) {
think.logger.error('saveMyInfo error:', error);


+ 30
- 0
src/model/sys_config.js Ver arquivo

@@ -0,0 +1,30 @@
module.exports = class extends think.Model {
get tableName() {
return 'sys_config';
}

/**
* 根据 key 获取配置值(自动 JSON.parse)
*/
async getByKey(key) {
const row = await this.where({ config_key: key }).find();
if (think.isEmpty(row)) return null;
try {
return JSON.parse(row.config_value);
} catch (e) {
return row.config_value;
}
}

/**
* 设置配置值(自动 JSON.stringify)
*/
async setByKey(key, value, remark) {
const val = typeof value === 'string' ? value : JSON.stringify(value);
const exists = await this.where({ config_key: key }).find();
if (think.isEmpty(exists)) {
return this.add({ config_key: key, config_value: val, remark: remark || '' });
}
return this.where({ config_key: key }).update({ config_value: val });
}
};

+ 59
- 0
src/service/ocr.js Ver arquivo

@@ -0,0 +1,59 @@
const cosConfig = require('../config/cos.js');

module.exports = class extends think.Service {
/**
* 对图片进行 OCR 识别,匹配瘤种关键词
* @param {string[]} imageUrls - 图片 URL 数组
* @param {string[]} tagOptions - 瘤种关键词列表
* @returns {string} 匹配到的第一个瘤种,未匹配返回空字符串
*/
async matchTag(imageUrls, tagOptions) {
if (!imageUrls || !imageUrls.length || !tagOptions || !tagOptions.length) return '';

for (const url of imageUrls) {
try {
const text = await this.recognizeText(url);
if (!text) continue;

// 按瘤种列表顺序匹配,优先匹配靠前的
for (const tag of tagOptions) {
// 去掉括号内容做模糊匹配,如 "胃肠道间质瘤(GIST)" 同时匹配 "胃肠道间质瘤" 和 "GIST"
const cleanTag = tag.replace(/[((][^))]*[))]/g, '').trim();
const bracketMatch = tag.match(/[((]([^))]*)[))]/);
if (text.includes(cleanTag)) return tag;
if (bracketMatch && bracketMatch[1] && text.toUpperCase().includes(bracketMatch[1].toUpperCase())) return tag;
if (text.includes(tag)) return tag;
}
} catch (err) {
think.logger.warn(`[OCR] 识别失败 ${url}:`, err.message);
}
}
return '';
}

/**
* 调用腾讯云通用印刷体 OCR
* @param {string} imageUrl - 图片 URL
* @returns {string} 识别出的全部文字
*/
async recognizeText(imageUrl) {
const tencentcloud = require('tencentcloud-sdk-nodejs');
const OcrClient = tencentcloud.ocr.v20181119.Client;

const client = new OcrClient({
credential: {
secretId: cosConfig.secretId,
secretKey: cosConfig.secretKey
},
region: 'ap-shanghai',
profile: {
httpProfile: { endpoint: 'ocr.tencentcloudapi.com' }
}
});

const result = await client.GeneralBasicOCR({ ImageUrl: imageUrl });
if (!result.TextDetections || !result.TextDetections.length) return '';

return result.TextDetections.map(item => item.DetectedText).join('');
}
};

+ 8
- 6
view/admin/patient_index.html Ver arquivo

@@ -258,18 +258,19 @@ const app = createApp({
// 省市区树形数据
const regionTree = ref([]);

// 瘤种选项
const tagOptions = [
'直肠癌', '结肠癌', '十二指肠癌', '空肠癌', '回肠癌',
'胃肠道间质瘤(GIST)', '肠道神经内分泌肿瘤',
'腺瘤性息肉', '炎性息肉', '增生性息肉', '平滑肌瘤', '脂肪瘤'
];
// 瘤种选项(从接口加载)
const tagOptions = ref([]);

const regionFilter = ref([]);
const exporting = ref(false);

const tabStatusMap = { all: '', draft: '-1', pending: '0', rejected: '2', approved: '1' };

async function loadTagOptions() {
var res = await fetch('/common/tagOptions').then(function(r) { return r.json(); });
if (res.code === 0) tagOptions.value = res.data || [];
}

async function loadRegionTree() {
var res = await fetch('/common/regions').then(function(r) { return r.json(); });
if (res.code === 0) regionTree.value = res.data || [];
@@ -472,6 +473,7 @@ const app = createApp({
onMounted(function() {
loadList();
loadRegionTree();
loadTagOptions();
});

return {


Carregando…
Cancelar
Salvar