This commit is contained in:
2025-11-18 10:35:53 +08:00
113 changed files with 16082 additions and 15768 deletions

View File

@@ -0,0 +1,23 @@
---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Implement an approved OpenSpec change and keep tasks in sync.
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
Track these steps as TODOs and complete them one by one.
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
**Reference**
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
<!-- OPENSPEC:END -->

View File

@@ -0,0 +1,27 @@
---
name: /openspec-archive
id: openspec-archive
category: OpenSpec
description: Archive a deployed OpenSpec change and update specs.
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
**Steps**
1. Determine the change ID to archive:
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
**Reference**
- Use `openspec list` to confirm change IDs before archiving.
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
<!-- OPENSPEC:END -->

View File

@@ -0,0 +1,27 @@
---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---
<!-- OPENSPEC:START -->
**Guardrails**
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
- Keep changes tightly scoped to the requested outcome.
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
**Steps**
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
**Reference**
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
<!-- OPENSPEC:END -->

18
AGENTS.md Normal file
View File

@@ -0,0 +1,18 @@
<!-- OPENSPEC:START -->
# OpenSpec Instructions
These instructions are for AI assistants working in this project.
Always open `@/openspec/AGENTS.md` when the request:
- Mentions planning or proposals (words like proposal, spec, change, plan)
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
- Sounds ambiguous and you need the authoritative spec before coding
Use `@/openspec/AGENTS.md` to learn:
- How to create and apply change proposals
- Spec format and conventions
- Project structure and guidelines
Keep this managed block so 'openspec update' can refresh the instructions.
<!-- OPENSPEC:END -->

View File

@@ -9,7 +9,7 @@
# 2. 应用类型是否为「浏览器端」
# 3. Referer白名单配置开发环境可设置为 *
# 4. API密钥状态是否为「启用」
VITE_BAIDU_MAP_API_KEY=SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo
VITE_BAIDU_MAP_API_KEY=fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC
# API服务地址
VITE_API_BASE_URL=http://localhost:5350/api

View File

@@ -16,7 +16,7 @@
### 百度地图配置
| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `VITE_BAIDU_MAP_API_KEY` | `SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo` | 百度地图API密钥 |
| `VITE_BAIDU_MAP_API_KEY` | `fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC` | 百度地图API密钥 |
### 应用配置
| 变量名 | 默认值 | 说明 |

View File

@@ -19,6 +19,7 @@
"moment": "^2.29.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"qrcode": "^1.5.4",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5"
@@ -1464,7 +1465,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -1473,7 +1473,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -1672,6 +1671,15 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
@@ -1730,6 +1738,51 @@
"node": "*"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
@@ -1742,7 +1795,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1753,8 +1805,7 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
@@ -1876,6 +1927,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deep-eql": {
"version": "4.1.4",
"resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -1957,6 +2017,12 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -2614,6 +2680,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz",
@@ -2926,7 +3001,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3548,6 +3622,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -3576,7 +3659,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -3694,6 +3776,15 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
@@ -3783,6 +3874,23 @@
"node": ">=6"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3809,6 +3917,21 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -4017,6 +4140,12 @@
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
@@ -4193,7 +4322,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -4855,6 +4983,12 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -5025,6 +5159,119 @@
"node": ">=12"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/yargs/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -5987,14 +6234,12 @@
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
@@ -6147,6 +6392,11 @@
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
@@ -6190,6 +6440,43 @@
"get-func-name": "^2.0.2"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
},
"dependencies": {
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
}
}
},
"codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
@@ -6199,7 +6486,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -6207,8 +6493,7 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"combined-stream": {
"version": "1.0.8",
@@ -6299,6 +6584,11 @@
"ms": "^2.1.3"
}
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
},
"deep-eql": {
"version": "4.1.4",
"resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz",
@@ -6353,6 +6643,11 @@
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
"dev": true
},
"dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -6847,6 +7142,11 @@
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz",
@@ -7060,8 +7360,7 @@
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"is-glob": {
"version": "4.0.3",
@@ -7516,6 +7815,11 @@
"p-limit": "^3.0.2"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -7540,8 +7844,7 @@
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
"path-is-absolute": {
"version": "1.0.1",
@@ -7622,6 +7925,11 @@
}
}
},
"pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
},
"postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
@@ -7678,6 +7986,16 @@
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true
},
"qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"requires": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
}
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -7690,6 +8008,16 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -7824,6 +8152,11 @@
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
@@ -7955,7 +8288,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.1"
}
@@ -8365,6 +8697,11 @@
"isexe": "^2.0.0"
}
},
"which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
},
"why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -8481,6 +8818,88 @@
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
"dev": true
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"requires": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"dependencies": {
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"requires": {
"p-locate": "^4.1.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"requires": {
"p-limit": "^2.2.0"
}
},
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
}
}
}
},
"yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -33,37 +33,38 @@
"deploy": "npm run build && npm run preview"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.0.6",
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"file-saver": "^2.0.5",
"lodash-es": "^4.17.21",
"moment": "^2.29.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"qrcode": "^1.5.4",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"xlsx": "^0.18.5",
"@ant-design/icons-vue": "^7.0.1",
"dayjs": "^1.11.10",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0"
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.6.2",
"vite": "^4.5.3",
"@types/node": "^16.18.68",
"@types/lodash-es": "^4.17.12",
"@types/node": "^16.18.68",
"@types/nprogress": "^0.2.3",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-vue": "^4.6.2",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/ui": "^0.34.6",
"@vue/eslint-config-typescript": "^11.0.3",
"eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2",
"rimraf": "^5.0.5",
"typescript": "^4.9.5",
"vite": "^4.5.3",
"vite-bundle-analyzer": "^0.7.0",
"vitest": "^0.34.6",
"@vitest/ui": "^0.34.6",
"@vitest/coverage-v8": "^0.34.6",
"vue-tsc": "^1.8.25"
}
}

3426
admin-system/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,10 @@ export const BAIDU_MAP_CONFIG = {
// 从环境变量读取API密钥如果没有则使用开发测试密钥
// 生产环境请设置 VITE_BAIDU_MAP_API_KEY 环境变量
// 请访问 http://lbsyun.baidu.com/apiconsole/key 申请有效的API密钥
apiKey: import.meta.env.VITE_BAIDU_MAP_API_KEY || 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo',
apiKey: import.meta.env.VITE_BAIDU_MAP_API_KEY || 'fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC',
// 备用API密钥用于不同环境
fallbackApiKey: import.meta.env.VITE_BAIDU_MAP_FALLBACK_KEY || 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo',
fallbackApiKey: import.meta.env.VITE_BAIDU_MAP_FALLBACK_KEY || 'fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC',
// 是否启用Referer校验开发环境建议关闭
enableRefererCheck: import.meta.env.VITE_BAIDU_MAP_ENABLE_REFERER !== 'false',

View File

@@ -31,7 +31,13 @@ const createHeaders = (headers = {}) => {
throw new Error('未认证,请先登录');
}
return { ...defaultHeaders, ...headers };
// 如果headers中明确设置了Content-Type为undefined则移除它
const mergedHeaders = { ...defaultHeaders, ...headers };
if (mergedHeaders['Content-Type'] === undefined) {
delete mergedHeaders['Content-Type'];
}
return mergedHeaders;
};
/**
@@ -342,16 +348,26 @@ export const api = {
/**
* POST请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object|FormData} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async post(endpoint, data, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
// 如果是FormData不设置Content-Type让浏览器自动处理
const isFormData = data instanceof FormData;
const headers = isFormData
? createHeaders({ ...options.headers, 'Content-Type': undefined })
: createHeaders(options.headers);
// 如果是FormData直接使用data否则使用JSON.stringify
const body = isFormData ? data : JSON.stringify(data);
const response = await fetch(url, {
method: 'POST',
headers: createHeaders(options.headers),
body: JSON.stringify(data),
headers: headers,
body: body,
...options,
});
return handleResponse(response);

View File

@@ -104,11 +104,12 @@ const generateRequestId = () => {
* @returns {boolean} 是否为标准格式
*/
export const isValidApiResponse = (response) => {
// 基本格式检查:必须有 success 和 message 字段
// timestamp 为可选字段(兼容旧接口)
return response &&
typeof response === 'object' &&
typeof response.success === 'boolean' &&
typeof response.message === 'string' &&
typeof response.timestamp === 'string';
(typeof response.message === 'string' || response.message === undefined);
};
/**

View File

@@ -14,7 +14,7 @@ const MAX_RETRY = 3;
* @param {string} apiKey - 百度地图API密钥
* @returns {Promise} 加载完成的Promise
*/
export const loadBaiduMapAPI = async (apiKey = 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo') => {
export const loadBaiduMapAPI = async (apiKey = 'fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC') => {
// 如果已经加载过,直接返回
if (BMapLoaded && window.BMap) {
console.log('百度地图API已加载');
@@ -118,7 +118,7 @@ export const loadBaiduMapAPI = async (apiKey = 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo
* @param {string} apiKey - 百度地图API密钥
* @returns {Promise} 加载完成的Promise
*/
export const retryLoadBaiduMapAPI = async (apiKey = 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo') => {
export const retryLoadBaiduMapAPI = async (apiKey = 'fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC') => {
if (retryCount >= MAX_RETRY) {
throw new Error('百度地图API加载重试次数已达上限');
}

View File

@@ -244,7 +244,7 @@ export class BaiduMapTester {
this.isLoading = true;
const script = document.createElement('script');
script.src = 'https://api.map.baidu.com/api?v=3.0&ak=SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo&callback=initBMapTest';
script.src = 'https://api.map.baidu.com/api?v=3.0&ak=fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC&callback=initBMapTest';
window.initBMapTest = () => {
this.isLoading = false;

View File

@@ -128,10 +128,10 @@
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="品" name="strain">
<a-form-item label="品" name="strain">
<a-select
v-model:value="formData.strain"
placeholder="请选择品"
placeholder="请选择品"
:loading="cattleUsersLoading"
@change="handleFieldChange('strain', $event)"
show-search
@@ -157,6 +157,7 @@
show-search
:filter-option="filterOption"
>
<!-- 动态选项 -->
<a-select-option
v-for="type in cattleTypes"
:key="type.id"
@@ -168,10 +169,10 @@
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="类别" name="cate">
<a-form-item label="生理阶段" name="cate">
<a-select
v-model:value="formData.cate"
placeholder="请选择类别"
placeholder="请选择生理阶段"
@change="handleFieldChange('cate', $event)"
show-search
:filter-option="filterCateOption"
@@ -240,16 +241,7 @@
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="生理阶段" name="physiologicalStage">
<a-input
v-model:value="formData.physiologicalStage"
placeholder="请输入生理阶段"
@input="handleFieldChange('physiologicalStage', $event.target.value)"
@change="handleFieldChange('physiologicalStage', $event.target.value)"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="胎次" name="parity">
<a-input-number
@@ -323,13 +315,19 @@
</a-col>
<a-col :span="12">
<a-form-item label="来源" name="source">
<a-input-number
<a-select
v-model:value="formData.source"
placeholder="请输入来源"
:min="0"
style="width: 100%"
placeholder="请选择来源"
@change="handleFieldChange('source', $event)"
/>
show-search
:filter-option="filterOption"
>
<a-select-option :value="1">购买</a-select-option>
<a-select-option :value="2">自繁</a-select-option>
<a-select-option :value="3">放生</a-select-option>
<a-select-option :value="4">合作社</a-select-option>
<a-select-option :value="5">入股</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
@@ -459,7 +457,7 @@ const formData = reactive({
penId: null, // 映射iot_cattle.pen_id
intoTime: null,
parity: 0,
source: 0,
source: '', // 映射iot_cattle.source改为空字符串以便验证
sourceDay: 0,
sourceWeight: 0,
ageInMonths: 0, // 从iot_cattle.birthday计算得出
@@ -484,7 +482,8 @@ const rules = {
cate: [{ required: true, message: '请输入类别', trigger: 'blur' }], // iot_cattle.cate
birthWeight: [{ required: true, message: '请输入出生体重', trigger: 'blur' }], // iot_cattle.birth_weight
birthday: [{ required: true, message: '请选择出生日期', trigger: 'change' }], // iot_cattle.birthday
orgId: [{ required: true, message: '请选择所属农场', trigger: 'change' }] // iot_cattle.org_id
orgId: [{ required: true, message: '请选择所属农场', trigger: 'change' }], // iot_cattle.org_id
source: [{ required: true, message: '请选择来源', trigger: 'change' }] // iot_cattle.source
}
// 表格列配置基于iot_cattle表字段映射
@@ -637,16 +636,34 @@ const fetchFarms = async () => {
const fetchPens = async (farmId = null) => {
try {
pensLoading.value = true
const token = localStorage.getItem('token')
const params = farmId ? { farmId } : {}
const response = await api.get('/iot-cattle/pens/list', {
params
})
if (response.success) {
pens.value = response.data
console.log('🔍 [牛只档案] 开始获取栏舍列表')
// 调用 /cattle-pens 接口
const params = {
page: 1,
pageSize: 10
}
// 如果有 farmId可以添加到参数中如果后端支持
if (farmId) {
params.farmId = farmId
}
console.log('📤 [牛只档案] 栏舍列表请求参数:', params)
const response = await api.cattlePens.getList(params)
console.log('📥 [牛只档案] 栏舍列表响应:', response)
if (response.success && response.data) {
// 从 response.data.list 中提取栏舍列表
const pensList = response.data.list || []
pens.value = pensList
console.log('✅ [牛只档案] 获取栏舍列表成功,共', pensList.length, '条')
return pensList
}
return []
} catch (error) {
console.error('获取栏舍列表失败:', error)
console.error('❌ [牛只档案] 获取栏舍列表失败:', error)
return []
} finally {
pensLoading.value = false
}
@@ -656,16 +673,37 @@ const fetchPens = async (farmId = null) => {
const fetchBatches = async (farmId = null) => {
try {
batchesLoading.value = true
const token = localStorage.getItem('token')
const params = farmId ? { farmId } : {}
const response = await api.get('/iot-cattle/batches/list', {
params
})
if (response.success) {
batches.value = response.data
console.log('🔍 [牛只档案] 开始获取批次列表')
// 调用 /cattle-batches 接口
const params = {
page: 1,
pageSize: 10,
search: '',
exactMatch: true,
strictMatch: true
}
// 如果有 farmId可以添加到参数中如果后端支持
if (farmId) {
params.farmId = farmId
}
console.log('📤 [牛只档案] 批次列表请求参数:', params)
const response = await api.cattleBatches.getList(params)
console.log('📥 [牛只档案] 批次列表响应:', response)
if (response.success && response.data) {
// 从 response.data.list 中提取批次列表
const batchesList = response.data.list || []
batches.value = batchesList
console.log('✅ [牛只档案] 获取批次列表成功,共', batchesList.length, '条')
return batchesList
}
return []
} catch (error) {
console.error('获取批次列表失败:', error)
console.error('❌ [牛只档案] 获取批次列表失败:', error)
return []
} finally {
batchesLoading.value = false
}
@@ -801,12 +839,27 @@ const initializeAddMode = () => {
// ==================== 编辑牛只档案功能 ====================
// 编辑牛只档案
const editAnimal = (record) => {
console.log('=== 点击编辑按钮 ===')
console.log('编辑记录:', record)
initializeEditMode(record)
openModal()
loadRequiredData()
// 编辑牛只档案
const editAnimal = async (record) => {
try {
console.log('=== 点击编辑按钮 ===')
console.log('编辑记录ID:', record.id)
// 调用后端接口获取详情
const response = await api.get(`/iot-cattle/${record.id}`)
if (response.success && response.data) {
console.log('获取到的详情数据:', response.data)
initializeEditMode(response.data)
openModal()
loadRequiredData()
} else {
message.error('获取牛只档案详情失败')
}
} catch (error) {
console.error('获取牛只档案详情失败:', error)
message.error(error.message || '获取牛只档案详情失败')
}
}
// 初始化编辑模式
@@ -842,27 +895,99 @@ const populateFormWithRecord = (record) => {
formData.id = record.id
formData.earNumber = record.earNumber || '' // iot_cattle.ear_number
formData.sex = record.sex || 1 // iot_cattle.sex
formData.strain = record.strain || '' // iot_cattle.strain
formData.varieties = record.varieties || '' // iot_cattle.varieties
formData.cate = record.cate || '' // iot_cattle.cate
// 使用原始IDstrainId如果没有则使用strain兼容旧数据
formData.strain = record.strainId !== undefined ? record.strainId : (record.strain || '') // iot_cattle.strain
// 使用原始IDvarietiesId如果没有则使用varieties兼容旧数据
formData.varieties = record.varietiesId !== undefined ? record.varietiesId : (record.varieties || '') // iot_cattle.varieties
// 使用原始IDcateId如果没有则使用cate兼容旧数据
formData.cate = record.cateId !== undefined ? record.cateId : (record.cate || '') // iot_cattle.cate
formData.birthWeight = record.birthWeight || 0 // iot_cattle.birth_weight
formData.birthday = record.birthday ? dayjs(record.birthday) : null // iot_cattle.birthday
formData.penId = record.penId || null // iot_cattle.pen_id
formData.intoTime = record.intoTime || null
// 处理出生日期如果是时间戳使用dayjs.unix如果是日期字符串使用dayjs
if (record.birthday) {
if (typeof record.birthday === 'number') {
// 时间戳转换为dayjs对象
const birthdayDate = dayjs.unix(record.birthday)
formData.birthday = birthdayDate.isValid() ? birthdayDate : null
console.log('转换出生日期:', record.birthday, '->', formData.birthday?.format('YYYY-MM-DD'))
} else {
formData.birthday = dayjs(record.birthday) // 日期字符串转换为dayjs对象
}
} else {
formData.birthday = null
}
// 处理所属栏舍如果为0或null则设为null
formData.penId = (record.penId && record.penId !== 0) ? record.penId : null // iot_cattle.pen_id
// 处理入场日期如果是时间戳使用dayjs.unix如果是日期字符串使用dayjs
if (record.intoTime) {
if (typeof record.intoTime === 'number') {
formData.intoTime = dayjs.unix(record.intoTime) // 时间戳转换为dayjs对象
} else {
formData.intoTime = dayjs(record.intoTime) // 日期字符串转换为dayjs对象
}
} else {
formData.intoTime = null
}
formData.parity = record.parity || 0
formData.source = record.source || 0
// 处理来源如果是数字包括0直接使用否则设为空字符串
if (typeof record.source === 'number') {
formData.source = record.source
} else if (record.source !== undefined && record.source !== null && record.source !== '') {
formData.source = record.source
} else {
formData.source = ''
}
formData.sourceDay = record.sourceDay || 0
formData.sourceWeight = record.sourceWeight || 0
formData.ageInMonths = calculateAgeInMonths(record.birthday) // 从iot_cattle.birthday计算月龄
// 优先使用JSON中的ageInMonths如果没有则计算
formData.ageInMonths = record.ageInMonths !== undefined ? record.ageInMonths : calculateAgeInMonths(record.birthday)
formData.physiologicalStage = record.physiologicalStage || ''
formData.currentWeight = record.currentWeight || 0
formData.weightCalculateTime = record.weightCalculateTime ? dayjs(record.weightCalculateTime) : null
// 处理体重计算时间如果是时间戳使用dayjs.unix如果是日期字符串使用dayjs
if (record.weightCalculateTime) {
if (typeof record.weightCalculateTime === 'number') {
formData.weightCalculateTime = dayjs.unix(record.weightCalculateTime) // 时间戳转换为dayjs对象
} else {
formData.weightCalculateTime = dayjs(record.weightCalculateTime) // 日期字符串转换为dayjs对象
}
} else {
formData.weightCalculateTime = null
}
formData.dayOfBirthday = record.dayOfBirthday || 0
formData.orgId = record.farmId || record.orgId || null // iot_cattle.org_id
formData.penId = record.penId || null // iot_cattle.pen_id
formData.batchId = record.batchId || null // iot_cattle.batch_id
// 保存 penId 和 batchId 的值,等待选项加载完成后再设置
const savedPenId = (record.penId && record.penId !== 0) ? record.penId : null
const savedBatchId = (record.batchId && record.batchId !== 0) ? record.batchId : null
console.log('填充后的 formData:', JSON.parse(JSON.stringify(formData)));
console.log('保存的 penId:', savedPenId, 'batchId:', savedBatchId);
console.log('出生日期转换结果:', formData.birthday);
// 如果所属农场有值,先加载对应的栏舍和批次选项,然后再设置值
if (formData.orgId) {
Promise.all([
fetchPens(formData.orgId),
fetchBatches(formData.orgId)
]).then(() => {
// 选项加载完成后再设置值,使用 nextTick 确保 DOM 更新
setTimeout(() => {
if (savedPenId !== null) {
formData.penId = savedPenId
console.log('设置 penId:', savedPenId, '当前栏舍选项:', pens.value)
}
if (savedBatchId !== null) {
formData.batchId = savedBatchId
console.log('设置 batchId:', savedBatchId, '当前批次选项:', batches.value)
}
}, 100) // 延迟100ms确保选项已加载到DOM
}).catch(error => {
console.error('加载栏舍或批次选项失败:', error)
})
} else {
// 如果没有农场ID直接设置值虽然可能没有对应的选项
formData.penId = savedPenId
formData.batchId = savedBatchId
}
}
// 配置编辑模式设置
@@ -932,7 +1057,7 @@ const handleSubmit = async () => {
console.log('原始表单数据:', JSON.parse(JSON.stringify(formData)));
// 检查必填字段
const requiredFields = ['earNumber', 'sex', 'strain', 'varieties', 'cate', 'birthWeight', 'birthday', 'orgId'];
const requiredFields = ['earNumber', 'sex', 'strain', 'varieties', 'cate', 'birthWeight', 'birthday', 'orgId', 'source'];
const missingFields = requiredFields.filter(field => !formData[field] && formData[field] !== 0);
if (missingFields.length > 0) {
console.error('缺少必填字段:', missingFields);
@@ -1006,7 +1131,7 @@ const resetForm = () => {
penId: null,
intoTime: null,
parity: 0,
source: 0,
source: '',
sourceDay: 0,
sourceWeight: 0,
ageInMonths: 0,
@@ -1268,12 +1393,8 @@ const confirmImport = async () => {
const formData = new FormData()
formData.append('file', importFile.value)
// 调用导入API
const response = await api.post('/iot-cattle/import', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
// 调用导入APIFormData会自动处理Content-Type不需要手动设置
const response = await api.post('/iot-cattle/import', formData)
if (response.data.success) {
message.destroy()
@@ -1290,7 +1411,16 @@ const confirmImport = async () => {
} catch (error) {
message.destroy()
console.error('导入失败:', error)
message.error('导入失败,请检查文件格式是否正确')
// 显示具体的错误信息
const errorMessage = error.message || error.response?.data?.message || '导入失败,请检查文件格式是否正确'
message.error(errorMessage)
// 如果是认证错误,提示用户重新登录
if (errorMessage.includes('认证') || errorMessage.includes('未授权') || errorMessage.includes('登录')) {
setTimeout(() => {
window.location.href = '/login'
}, 2000)
}
} finally {
importLoading.value = false
}

View File

@@ -110,27 +110,22 @@
>
<a-form-item label="批次名称" name="name">
<a-input
v-model="formData.name"
v-model:value="formData.name"
placeholder="请输入批次名称"
@input="handleFieldChange('name', $event.target.value)"
@change="handleFieldChange('name', $event.target.value)"
/>
</a-form-item>
<a-form-item label="批次编号" name="code">
<a-input
v-model="formData.code"
v-model:value="formData.code"
placeholder="请输入批次编号"
@input="handleFieldChange('code', $event.target.value)"
@change="handleFieldChange('code', $event.target.value)"
/>
</a-form-item>
<a-form-item label="批次类型" name="type">
<a-select
v-model="formData.type"
v-model:value="formData.type"
placeholder="请选择批次类型"
@change="handleFieldChange('type', $event)"
>
<a-select-option value="育成批次">育成批次</a-select-option>
<a-select-option value="繁殖批次">繁殖批次</a-select-option>
@@ -142,66 +137,58 @@
<a-form-item label="开始日期" name="startDate">
<a-date-picker
v-model="formData.startDate"
v-model:value="formData.startDate"
style="width: 100%"
placeholder="请选择开始日期"
@change="handleFieldChange('startDate', $event)"
/>
</a-form-item>
<a-form-item label="预计结束日期" name="expectedEndDate">
<a-date-picker
v-model="formData.expectedEndDate"
v-model:value="formData.expectedEndDate"
style="width: 100%"
placeholder="请选择预计结束日期"
@change="handleFieldChange('expectedEndDate', $event)"
/>
</a-form-item>
<a-form-item label="实际结束日期" name="actualEndDate">
<a-date-picker
v-model="formData.actualEndDate"
v-model:value="formData.actualEndDate"
style="width: 100%"
placeholder="请选择实际结束日期"
@change="handleFieldChange('actualEndDate', $event)"
/>
</a-form-item>
<a-form-item label="目标牛只数量" name="targetCount">
<a-input-number
v-model="formData.targetCount"
v-model:value="formData.targetCount"
:min="1"
:max="1000"
style="width: 100%"
placeholder="请输入目标牛只数量"
@change="handleFieldChange('targetCount', $event)"
/>
</a-form-item>
<a-form-item label="当前牛只数量" name="currentCount">
<a-input-number
v-model="formData.currentCount"
v-model:value="formData.currentCount"
:min="0"
:max="formData.targetCount || 1000"
style="width: 100%"
placeholder="当前牛只数量"
@change="handleFieldChange('currentCount', $event)"
/>
</a-form-item>
<a-form-item label="负责人" name="manager">
<a-input
v-model="formData.manager"
v-model:value="formData.manager"
placeholder="请输入负责人姓名"
@input="handleFieldChange('manager', $event.target.value)"
@change="handleFieldChange('manager', $event.target.value)"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group
v-model="formData.status"
@change="handleFieldChange('status', $event.target.value)"
v-model:value="formData.status"
>
<a-radio value="进行中">进行中</a-radio>
<a-radio value="已完成">已完成</a-radio>
@@ -211,11 +198,9 @@
<a-form-item label="备注" name="remark">
<a-textarea
v-model="formData.remark"
v-model:value="formData.remark"
:rows="3"
placeholder="请输入备注信息"
@input="handleFieldChange('remark', $event.target.value)"
@change="handleFieldChange('remark', $event.target.value)"
/>
</a-form-item>
</a-form>
@@ -646,7 +631,7 @@ const handleAdd = () => {
modalVisible.value = true
}
const handleEdit = (record) => {
const handleEdit = async (record) => {
console.log('🔄 [批次设置] 开始编辑操作')
console.log('📋 [批次设置] 原始记录数据:', {
id: record.id,
@@ -663,15 +648,76 @@ const handleEdit = (record) => {
})
modalTitle.value = '编辑批次'
Object.assign(formData, {
...record,
startDate: record.startDate ? dayjs(record.startDate) : null,
expectedEndDate: record.expectedEndDate ? dayjs(record.expectedEndDate) : null,
actualEndDate: record.actualEndDate ? dayjs(record.actualEndDate) : null
})
// 处理日期转换
let startDateValue = null
let expectedEndDateValue = null
let actualEndDateValue = null
if (record.startDate) {
if (typeof record.startDate === 'string') {
startDateValue = dayjs(record.startDate)
} else if (record.startDate instanceof Date) {
startDateValue = dayjs(record.startDate)
} else if (record.startDate && typeof record.startDate === 'object' && record.startDate.format) {
startDateValue = record.startDate
} else {
startDateValue = dayjs(record.startDate)
}
}
if (record.expectedEndDate) {
if (typeof record.expectedEndDate === 'string') {
expectedEndDateValue = dayjs(record.expectedEndDate)
} else if (record.expectedEndDate instanceof Date) {
expectedEndDateValue = dayjs(record.expectedEndDate)
} else if (record.expectedEndDate && typeof record.expectedEndDate === 'object' && record.expectedEndDate.format) {
expectedEndDateValue = record.expectedEndDate
} else {
expectedEndDateValue = dayjs(record.expectedEndDate)
}
}
if (record.actualEndDate) {
if (typeof record.actualEndDate === 'string') {
actualEndDateValue = dayjs(record.actualEndDate)
} else if (record.actualEndDate instanceof Date) {
actualEndDateValue = dayjs(record.actualEndDate)
} else if (record.actualEndDate && typeof record.actualEndDate === 'object' && record.actualEndDate.format) {
actualEndDateValue = record.actualEndDate
} else {
actualEndDateValue = dayjs(record.actualEndDate)
}
}
// 填充表单数据 - 使用直接赋值确保响应式更新
formData.id = record.id
formData.name = record.name || ''
formData.code = record.code || ''
formData.type = record.type || ''
formData.startDate = startDateValue
formData.expectedEndDate = expectedEndDateValue
formData.actualEndDate = actualEndDateValue
formData.targetCount = record.targetCount || 0
formData.currentCount = record.currentCount || 0
formData.manager = record.manager || ''
formData.status = record.status || '进行中'
formData.remark = record.remark || ''
formData.farmId = record.farmId || record.farm_id || null
console.log('📝 [批次设置] 表单数据已填充:', formData)
console.log('📝 [批次设置] startDate 是否为 dayjs:', formData.startDate && typeof formData.startDate.format === 'function')
console.log('📝 [批次设置] expectedEndDate 是否为 dayjs:', formData.expectedEndDate && typeof formData.expectedEndDate.format === 'function')
console.log('📝 [批次设置] actualEndDate 是否为 dayjs:', formData.actualEndDate && typeof formData.actualEndDate.format === 'function')
// 打开模态框
modalVisible.value = true
// 等待模态框和表单渲染完成
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
console.log('✅ [批次设置] 模态框已打开,表单应该已填充')
}
const handleDelete = (record) => {

View File

@@ -126,7 +126,7 @@
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<a-button type="link" size="small" @click="() => handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleViewDetails(record)">
@@ -167,27 +167,23 @@
>
<a-form-item label="牛只耳号" name="earNumber">
<a-input
v-model="formData.earNumber"
v-model:value="formData.earNumber"
placeholder="请输入牛只耳号"
@input="handleFieldChange('earNumber', $event.target.value)"
@change="handleFieldChange('earNumber', $event.target.value)"
/>
</a-form-item>
<a-form-item label="离栏日期" name="exitDate">
<a-date-picker
v-model="formData.exitDate"
v-model:value="formData.exitDate"
style="width: 100%"
placeholder="请选择离栏日期"
@change="handleFieldChange('exitDate', $event)"
/>
</a-form-item>
<a-form-item label="离栏原因" name="exitReason">
<a-select
v-model="formData.exitReason"
v-model:value="formData.exitReason"
placeholder="请选择离栏原因"
@change="handleFieldChange('exitReason', $event)"
>
<a-select-option value="出售">出售</a-select-option>
<a-select-option value="死亡">死亡</a-select-option>
@@ -199,9 +195,8 @@
<a-form-item label="原栏舍" name="originalPenId">
<a-select
v-model="formData.originalPenId"
v-model:value="formData.originalPenId"
placeholder="请选择原栏舍"
@change="handleFieldChange('originalPenId', $event)"
>
<a-select-option
v-for="pen in penList"
@@ -215,18 +210,15 @@
<a-form-item label="去向" name="destination">
<a-input
v-model="formData.destination"
v-model:value="formData.destination"
placeholder="请输入牛只去向"
@input="handleFieldChange('destination', $event.target.value)"
@change="handleFieldChange('destination', $event.target.value)"
/>
</a-form-item>
<a-form-item label="处理方式" name="disposalMethod">
<a-select
v-model="formData.disposalMethod"
v-model:value="formData.disposalMethod"
placeholder="请选择处理方式"
@change="handleFieldChange('disposalMethod', $event)"
>
<a-select-option value="屠宰">屠宰</a-select-option>
<a-select-option value="转售">转售</a-select-option>
@@ -238,17 +230,14 @@
<a-form-item label="处理人员" name="handler">
<a-input
v-model="formData.handler"
v-model:value="formData.handler"
placeholder="请输入处理人员姓名"
@input="handleFieldChange('handler', $event.target.value)"
@change="handleFieldChange('handler', $event.target.value)"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group
v-model="formData.status"
@change="handleFieldChange('status', $event.target.value)"
v-model:value="formData.status"
>
<a-radio value="已确认">已确认</a-radio>
<a-radio value="待确认">待确认</a-radio>
@@ -258,11 +247,9 @@
<a-form-item label="备注" name="remark">
<a-textarea
v-model="formData.remark"
v-model:value="formData.remark"
:rows="3"
placeholder="请输入备注信息"
@input="handleFieldChange('remark', $event.target.value)"
@change="handleFieldChange('remark', $event.target.value)"
/>
</a-form-item>
</a-form>
@@ -579,38 +566,207 @@ const handleAdd = () => {
modalVisible.value = true
}
const handleEdit = (record) => {
console.log('🔄 [离栏记录] 开始编辑操作')
console.log('📋 [离栏记录] 原始记录数据:', {
id: record.id,
earNumber: record.earNumber,
exitDate: record.exitDate,
exitReason: record.exitReason,
originalPenId: record.originalPenId,
originalPenName: record.originalPenName,
destination: record.destination,
disposalMethod: record.disposalMethod,
handler: record.handler,
status: record.status,
remark: record.remark
})
// 获取离栏记录详情(用于编辑)
const getExitRecordDetail = async (id) => {
console.log('🔵 [离栏记录-编辑] ========== 开始获取详情 ==========')
console.log('🔵 [离栏记录-编辑] 函数被调用记录ID:', id)
console.log('🔵 [离栏记录-编辑] 调用时间:', new Date().toISOString())
modalTitle.value = '编辑离栏记录'
Object.assign(formData, {
id: record.id,
earNumber: record.earNumber, // 映射耳号字段
exitDate: record.exitDate ? dayjs(record.exitDate) : null,
exitReason: record.exitReason,
originalPenId: record.originalPenId, // 映射原栏舍ID
destination: record.destination,
disposalMethod: record.disposalMethod,
handler: record.handler,
status: record.status,
remark: record.remark || ''
})
try {
console.log('🔄 [离栏记录-编辑] 开始获取详情')
console.log('📋 [离栏记录-编辑] 记录ID:', id)
// 调用后端接口获取详情
console.log('📡 [离栏记录-编辑] 准备调用 API: /cattle-exit-records/' + id)
const response = await api.cattleExitRecords.getById(id)
console.log('📡 [离栏记录-编辑] API 调用完成,响应状态:', response.success)
if (response.success && response.data) {
console.log('✅ [离栏记录-编辑] 获取详情成功:', response.data)
console.log('🔵 [离栏记录-编辑] ========== 获取详情成功 ==========')
return response.data
} else {
console.error('❌ [离栏记录-编辑] 获取详情失败:', response.message)
throw new Error(response.message || '获取离栏记录详情失败')
}
} catch (error) {
console.error('❌ [离栏记录-编辑] 获取详情异常:', error)
console.error('🔵 [离栏记录-编辑] ========== 获取详情失败 ==========')
throw error
}
}
// 编辑离栏记录
const handleEdit = async (record) => {
// 立即输出日志,确认函数被调用
console.log('🟢🟢🟢 [离栏记录-编辑] ========== handleEdit 函数被调用 ==========')
console.log('🟢 [离栏记录-编辑] 调用时间:', new Date().toISOString())
console.log('🟢 [离栏记录-编辑] 记录数据:', record)
console.log('🟢 [离栏记录-编辑] 记录ID:', record?.id)
console.log('📝 [离栏记录] 表单数据已填充:', formData)
modalVisible.value = true
// 验证 record 对象
if (!record) {
console.error('❌ [离栏记录-编辑] record 为空')
message.error('记录数据为空')
return
}
if (!record.id) {
console.error('❌ [离栏记录-编辑] record.id 为空')
message.error('记录ID为空')
return
}
try {
console.log('🔄 [离栏记录-编辑] 开始编辑操作')
console.log('📋 [离栏记录-编辑] 编辑记录ID:', record.id)
// 调用封装的获取详情接口
console.log('📞 [离栏记录-编辑] 准备调用 getExitRecordDetail 函数ID:', record.id)
console.log('📞 [离栏记录-编辑] getExitRecordDetail 函数是否存在:', typeof getExitRecordDetail)
const detailData = await getExitRecordDetail(record.id)
console.log('📞 [离栏记录-编辑] getExitRecordDetail 函数调用完成,返回数据:', detailData)
if (detailData) {
console.log('📋 [离栏记录] 获取到的详情数据:', detailData)
modalTitle.value = '编辑离栏记录'
// 确保栏舍列表已加载
if (penList.value.length === 0) {
await loadPenList()
}
// 填充表单数据 - 直接赋值给 reactive 对象的属性
// 处理日期转换
let exitDateValue = null
if (detailData.exitDate) {
// 如果是字符串,转换为 dayjs 对象
if (typeof detailData.exitDate === 'string') {
exitDateValue = dayjs(detailData.exitDate)
} else if (detailData.exitDate instanceof Date) {
exitDateValue = dayjs(detailData.exitDate)
} else if (detailData.exitDate && typeof detailData.exitDate === 'object' && detailData.exitDate.format) {
// 如果已经是 dayjs 对象,直接使用
exitDateValue = detailData.exitDate
} else {
exitDateValue = dayjs(detailData.exitDate)
}
console.log('📅 [离栏记录] 日期转换:', detailData.exitDate, '->', exitDateValue?.format('YYYY-MM-DD'))
console.log('📅 [离栏记录] exitDateValue 类型:', typeof exitDateValue, '是否为 dayjs:', exitDateValue && typeof exitDateValue.format === 'function')
}
// 先填充表单数据(在打开模态框之前)
console.log('📝 [离栏记录] 开始填充表单数据')
// 直接赋值给 reactive 对象
formData.id = detailData.id || null
formData.earNumber = detailData.earNumber || ''
formData.exitDate = exitDateValue // 确保是 dayjs 对象
formData.exitReason = detailData.exitReason || ''
formData.originalPenId = detailData.originalPenId || null
formData.destination = detailData.destination || ''
formData.disposalMethod = detailData.disposalMethod || ''
formData.handler = detailData.handler || ''
formData.status = detailData.status || '待确认'
formData.remark = detailData.remark || ''
console.log('📝 [离栏记录] 表单数据已填充到 formData')
console.log('📝 [离栏记录] formData.exitDate:', formData.exitDate)
console.log('📝 [离栏记录] formData.exitDate 类型:', typeof formData.exitDate)
console.log('📝 [离栏记录] formData.exitDate 是否为 dayjs:', formData.exitDate && typeof formData.exitDate.format === 'function')
console.log('📝 [离栏记录] formData 完整数据:', {
id: formData.id,
earNumber: formData.earNumber,
exitDate: formData.exitDate ? (formData.exitDate.format ? formData.exitDate.format('YYYY-MM-DD') : formData.exitDate) : null,
exitReason: formData.exitReason,
originalPenId: formData.originalPenId,
destination: formData.destination,
disposalMethod: formData.disposalMethod,
handler: formData.handler,
status: formData.status,
remark: formData.remark
})
// 先打开模态框
modalVisible.value = true
console.log('📝 [离栏记录] 模态框已打开')
// 等待模态框完全打开和表单渲染
await nextTick()
await new Promise(resolve => setTimeout(resolve, 300))
// 确保日期字段是 dayjs 对象(防止被序列化)
if (detailData.exitDate && (!formData.exitDate || typeof formData.exitDate.format !== 'function')) {
formData.exitDate = exitDateValue
console.log('🔄 [离栏记录] 重新设置 exitDate 为 dayjs 对象:', formData.exitDate)
}
console.log('🔍 [离栏记录] 检查 formData.exitDate 是否为 dayjs:',
formData.exitDate && typeof formData.exitDate.format === 'function' ? '是' : '否',
formData.exitDate ? (formData.exitDate.format ? formData.exitDate.format('YYYY-MM-DD') : formData.exitDate) : 'null'
)
// 由于表单使用 v-model 绑定到 formData直接修改 formData 即可更新表单
// 但为了确保表单组件正确响应,我们尝试使用 setFieldsValue如果可用
if (formRef.value) {
// 检查表单实例是否有 setFieldsValue 方法
if (typeof formRef.value.setFieldsValue === 'function') {
try {
const fieldsValue = {
id: formData.id,
earNumber: formData.earNumber,
exitDate: formData.exitDate, // 确保是 dayjs 对象
exitReason: formData.exitReason,
originalPenId: formData.originalPenId,
destination: formData.destination,
disposalMethod: formData.disposalMethod,
handler: formData.handler,
status: formData.status,
remark: formData.remark
}
console.log('🔧 [离栏记录] 使用 setFieldsValue 设置表单值')
console.log('🔧 [离栏记录] exitDate 值:', fieldsValue.exitDate, '类型:', typeof fieldsValue.exitDate)
formRef.value.setFieldsValue(fieldsValue)
console.log('✅ [离栏记录] setFieldsValue 设置成功')
} catch (error) {
console.warn('⚠️ [离栏记录] setFieldsValue 失败,但 formData 已更新,表单应该会自动响应:', error)
}
} else {
console.log(' [离栏记录] 表单实例没有 setFieldsValue 方法,使用 v-model 绑定formData 已更新)')
}
} else {
console.warn('⚠️ [离栏记录] formRef.value 为空,但 formData 已更新,表单应该会自动响应')
}
// 再次使用 nextTick 确保值已更新
await nextTick()
await new Promise(resolve => setTimeout(resolve, 100))
// 验证表单值
console.log('✅ [离栏记录] 模态框已打开')
console.log('✅ [离栏记录] formData.exitDate 最终值:', formData.exitDate)
console.log('✅ [离栏记录] formData.exitDate 是否为 dayjs:', formData.exitDate && typeof formData.exitDate.format === 'function')
// 如果表单实例有 getFieldsValue 方法,验证表单值
if (formRef.value && typeof formRef.value.getFieldsValue === 'function') {
try {
const currentFormData = formRef.value.getFieldsValue()
console.log('✅ [离栏记录] 表单当前值(通过 getFieldsValue:', currentFormData)
console.log('✅ [离栏记录] 表单 exitDate 值:', currentFormData.exitDate)
} catch (error) {
console.warn('⚠️ [离栏记录] 无法获取表单值:', error)
}
}
} else {
message.error('获取离栏记录详情失败')
}
} catch (error) {
console.error('❌ [离栏记录] 编辑操作失败:', error)
message.error(error.message || '获取离栏记录详情失败')
}
}
const handleDelete = (record) => {

View File

@@ -97,27 +97,22 @@
>
<a-form-item label="栏舍名称" name="name">
<a-input
v-model="formData.name"
v-model:value="formData.name"
placeholder="请输入栏舍名称"
@input="handleFieldChange('name', $event.target.value)"
@change="handleFieldChange('name', $event.target.value)"
/>
</a-form-item>
<a-form-item label="栏舍编号" name="code">
<a-input
v-model="formData.code"
v-model:value="formData.code"
placeholder="请输入栏舍编号"
@input="handleFieldChange('code', $event.target.value)"
@change="handleFieldChange('code', $event.target.value)"
/>
</a-form-item>
<a-form-item label="栏舍类型" name="type">
<a-select
v-model="formData.type"
v-model:value="formData.type"
placeholder="请选择栏舍类型"
@change="handleFieldChange('type', $event)"
>
<a-select-option value="育成栏">育成栏</a-select-option>
<a-select-option value="产房">产房</a-select-option>
@@ -129,51 +124,45 @@
<a-form-item label="容量" name="capacity">
<a-input-number
v-model="formData.capacity"
v-model:value="formData.capacity"
:min="1"
:max="1000"
style="width: 100%"
placeholder="请输入栏舍容量"
@change="handleFieldChange('capacity', $event)"
/>
</a-form-item>
<a-form-item label="当前牛只数量" name="currentCount">
<a-input-number
v-model="formData.currentCount"
v-model:value="formData.currentCount"
:min="0"
:max="formData.capacity || 1000"
style="width: 100%"
placeholder="当前牛只数量"
@change="handleFieldChange('currentCount', $event)"
/>
</a-form-item>
<a-form-item label="面积(平方米)" name="area">
<a-input-number
v-model="formData.area"
v-model:value="formData.area"
:min="0"
:precision="2"
style="width: 100%"
placeholder="请输入栏舍面积"
@change="handleFieldChange('area', $event)"
/>
</a-form-item>
<a-form-item label="位置描述" name="location">
<a-textarea
v-model="formData.location"
v-model:value="formData.location"
:rows="3"
placeholder="请输入栏舍位置描述"
@input="handleFieldChange('location', $event.target.value)"
@change="handleFieldChange('location', $event.target.value)"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group
v-model="formData.status"
@change="handleFieldChange('status', $event.target.value)"
v-model:value="formData.status"
>
<a-radio value="启用">启用</a-radio>
<a-radio value="停用">停用</a-radio>
@@ -182,11 +171,9 @@
<a-form-item label="备注" name="remark">
<a-textarea
v-model="formData.remark"
v-model:value="formData.remark"
:rows="3"
placeholder="请输入备注信息"
@input="handleFieldChange('remark', $event.target.value)"
@change="handleFieldChange('remark', $event.target.value)"
/>
</a-form-item>
</a-form>
@@ -219,7 +206,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { message, Modal } from 'ant-design-vue'
import {
PlusOutlined,
@@ -415,9 +402,9 @@ const loadData = async () => {
pageSize: pagination.pageSize
}
// 精确匹配栏舍名称
if (searchValue.value) {
params.name = searchValue.value
// 搜索关键词(后端使用 search 参数)
if (searchValue.value && searchValue.value.trim()) {
params.search = searchValue.value.trim()
}
console.log('📤 [栏舍设置] 发送请求参数:', params)
@@ -454,7 +441,7 @@ const handleAdd = () => {
modalVisible.value = true
}
const handleEdit = (record) => {
const handleEdit = async (record) => {
console.log('🔄 [栏舍设置] 开始编辑操作')
console.log('📋 [栏舍设置] 原始记录数据:', {
id: record.id,
@@ -471,10 +458,30 @@ const handleEdit = (record) => {
})
modalTitle.value = '编辑栏舍'
Object.assign(formData, record)
// 填充表单数据 - 使用直接赋值确保响应式更新
formData.id = record.id || null
formData.name = record.name || ''
formData.code = record.code || ''
formData.type = record.type || ''
formData.capacity = record.capacity || 0
formData.currentCount = record.currentCount || 0
formData.area = record.area || null
formData.location = record.location || ''
formData.status = record.status || '启用'
formData.remark = record.remark || ''
formData.farmId = record.farmId || record.farm_id || null
console.log('📝 [栏舍设置] 表单数据已填充:', formData)
// 打开模态框
modalVisible.value = true
// 等待模态框和表单渲染完成
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
console.log('✅ [栏舍设置] 模态框已打开,表单应该已填充')
}
const handleDelete = (record) => {

View File

@@ -168,18 +168,15 @@
>
<a-form-item label="牛只耳号" name="earNumber">
<a-input
v-model="formData.earNumber"
v-model:value="formData.earNumber"
placeholder="请输入牛只耳号"
@input="handleFieldChange('earNumber', $event.target.value)"
@change="handleFieldChange('earNumber', $event.target.value)"
/>
</a-form-item>
<a-form-item label="转出栏舍" name="fromPenId">
<a-select
v-model="formData.fromPenId"
v-model:value="formData.fromPenId"
placeholder="请选择转出栏舍"
@change="handleFieldChange('fromPenId', $event)"
>
<a-select-option
v-for="pen in penList"
@@ -193,9 +190,8 @@
<a-form-item label="转入栏舍" name="toPenId">
<a-select
v-model="formData.toPenId"
v-model:value="formData.toPenId"
placeholder="请选择转入栏舍"
@change="handleFieldChange('toPenId', $event)"
>
<a-select-option
v-for="pen in penList"
@@ -209,18 +205,16 @@
<a-form-item label="转栏日期" name="transferDate">
<a-date-picker
v-model="formData.transferDate"
v-model:value="formData.transferDate"
style="width: 100%"
placeholder="请选择转栏日期"
@change="handleFieldChange('transferDate', $event)"
/>
</a-form-item>
<a-form-item label="转栏原因" name="reason">
<a-select
v-model="formData.reason"
v-model:value="formData.reason"
placeholder="请选择转栏原因"
@change="handleFieldChange('reason', $event)"
>
<a-select-option value="正常调栏">正常调栏</a-select-option>
<a-select-option value="疾病治疗">疾病治疗</a-select-option>
@@ -233,17 +227,14 @@
<a-form-item label="操作人员" name="operator">
<a-input
v-model="formData.operator"
v-model:value="formData.operator"
placeholder="请输入操作人员姓名"
@input="handleFieldChange('operator', $event.target.value)"
@change="handleFieldChange('operator', $event.target.value)"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group
v-model="formData.status"
@change="handleFieldChange('status', $event.target.value)"
v-model:value="formData.status"
>
<a-radio value="已完成">已完成</a-radio>
<a-radio value="进行中">进行中</a-radio>
@@ -252,11 +243,9 @@
<a-form-item label="备注" name="remark">
<a-textarea
v-model="formData.remark"
v-model:value="formData.remark"
:rows="3"
placeholder="请输入备注信息"
@input="handleFieldChange('remark', $event.target.value)"
@change="handleFieldChange('remark', $event.target.value)"
/>
</a-form-item>
</a-form>
@@ -567,7 +556,7 @@ const handleAdd = () => {
modalVisible.value = true
}
const handleEdit = (record) => {
const handleEdit = async (record) => {
console.log('🔄 [转栏记录] 开始编辑操作')
console.log('📋 [转栏记录] 原始记录数据:', {
id: record.id,
@@ -584,20 +573,50 @@ const handleEdit = (record) => {
})
modalTitle.value = '编辑转栏记录'
Object.assign(formData, {
id: record.id,
earNumber: record.earNumber,
fromPenId: record.fromPenId,
toPenId: record.toPenId,
transferDate: record.transferDate ? dayjs(record.transferDate) : null,
reason: record.reason,
operator: record.operator,
status: record.status,
remark: record.remark || ''
})
// 确保栏舍列表已加载
if (penList.value.length === 0) {
await loadPenList()
}
// 处理日期转换
let transferDateValue = null
if (record.transferDate) {
if (typeof record.transferDate === 'string') {
transferDateValue = dayjs(record.transferDate)
} else if (record.transferDate instanceof Date) {
transferDateValue = dayjs(record.transferDate)
} else if (record.transferDate && typeof record.transferDate === 'object' && record.transferDate.format) {
// 如果已经是 dayjs 对象,直接使用
transferDateValue = record.transferDate
} else {
transferDateValue = dayjs(record.transferDate)
}
console.log('📅 [转栏记录] 日期转换:', record.transferDate, '->', transferDateValue?.format('YYYY-MM-DD'))
}
// 填充表单数据
formData.id = record.id
formData.earNumber = record.earNumber || ''
formData.fromPenId = record.fromPenId || null
formData.toPenId = record.toPenId || null
formData.transferDate = transferDateValue
formData.reason = record.reason || ''
formData.operator = record.operator || ''
formData.status = record.status || '进行中'
formData.remark = record.remark || ''
console.log('📝 [转栏记录] 表单数据已填充:', formData)
console.log('📝 [转栏记录] transferDate 是否为 dayjs:', formData.transferDate && typeof formData.transferDate.format === 'function')
// 打开模态框
modalVisible.value = true
// 等待模态框和表单渲染完成
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
console.log('✅ [转栏记录] 模态框已打开,表单应该已填充')
}
const handleDelete = (record) => {

View File

@@ -95,19 +95,16 @@
<a-col :span="12">
<a-form-item label="栏舍名" name="name" required>
<a-input
v-model="formData.name"
placeholder="请输入栏舍名"
@input="(e) => { console.log('栏舍名输入:', e.target.value); formData.name = e.target.value; }"
@change="(e) => { console.log('栏舍名变化:', e.target.value); }"
v-model:value="formData.name"
placeholder="请输入栏舍名"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="动物类型" name="animal_type" required>
<a-select
v-model="formData.animal_type"
v-model:value="formData.animal_type"
placeholder="请选择动物类型"
@change="(value) => { console.log('动物类型变化:', value); }"
>
<a-select-option value="马"></a-select-option>
<a-select-option value="牛"></a-select-option>
@@ -123,20 +120,16 @@
<a-col :span="12">
<a-form-item label="栏舍类型" name="pen_type">
<a-input
v-model="formData.pen_type"
v-model:value="formData.pen_type"
placeholder="请输入栏舍类型"
@input="(e) => { console.log('栏舍类型输入:', e.target.value); formData.pen_type = e.target.value; }"
@change="(e) => { console.log('栏舍类型变化:', e.target.value); }"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="负责人" name="responsible" required>
<a-input
v-model="formData.responsible"
v-model:value="formData.responsible"
placeholder="请输入负责人"
@input="(e) => { console.log('负责人输入:', e.target.value); formData.responsible = e.target.value; }"
@change="(e) => { console.log('负责人变化:', e.target.value); }"
/>
</a-form-item>
</a-col>
@@ -146,20 +139,18 @@
<a-col :span="12">
<a-form-item label="容量" name="capacity" required>
<a-input-number
v-model="formData.capacity"
v-model:value="formData.capacity"
:min="1"
:max="10000"
placeholder="请输入容量"
style="width: 100%"
@change="(value) => { console.log('容量变化:', value); formData.capacity = value; }"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-switch
:checked="formData.status"
@change="(checked) => { console.log('状态变化:', checked); formData.status = checked; }"
v-model:checked="formData.status"
:checked-children="'开启'"
:un-checked-children="'关闭'"
/>
@@ -169,11 +160,9 @@
<a-form-item label="备注" name="description">
<a-textarea
v-model="formData.description"
v-model:value="formData.description"
placeholder="请输入备注信息"
:rows="3"
@input="(e) => { console.log('备注输入:', e.target.value); formData.description = e.target.value; }"
@change="(e) => { console.log('备注变化:', e.target.value); }"
/>
</a-form-item>
</a-form>
@@ -182,7 +171,7 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
import { api } from '../utils/api'
@@ -364,21 +353,21 @@ const showAddModal = () => {
modalVisible.value = true
}
const handleEdit = (record) => {
const handleEdit = async (record) => {
console.log('=== 开始编辑栏舍 ===')
console.log('点击编辑按钮,原始记录数据:', record)
isEdit.value = true
Object.assign(formData, {
id: record.id,
name: record.name,
animal_type: record.animal_type,
pen_type: record.pen_type,
responsible: record.responsible,
capacity: record.capacity,
status: record.status,
description: record.description
})
// 填充表单数据 - 使用直接赋值确保响应式更新
formData.id = record.id || null
formData.name = record.name || ''
formData.animal_type = record.animal_type || ''
formData.pen_type = record.pen_type || ''
formData.responsible = record.responsible || ''
formData.capacity = record.capacity || 0
formData.status = record.status || false
formData.description = record.description || ''
console.log('编辑模式:表单数据已填充')
console.log('formData对象:', formData)
@@ -390,8 +379,14 @@ const handleEdit = (record) => {
console.log('formData.status:', formData.status)
console.log('formData.description:', formData.description)
// 打开模态框
modalVisible.value = true
console.log('编辑模态框已打开')
// 等待模态框和表单渲染完成
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
console.log('编辑模态框已打开,表单应该已填充')
}
const handleDelete = (record) => {

View File

@@ -166,24 +166,24 @@
<!-- 操作 -->
<template v-else-if="column.dataIndex === 'action'">
<div style="display: flex; flex-wrap: wrap; gap: 2px;">
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="editDevice(record)">
<!-- <a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="editDevice(record)">
修改绑定
</a-button>
</a-button> -->
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="clearData(record)">
溯源二维码
</a-button>
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="bindDevice(record)">
绑定信息
</a-button>
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="setDefault(record)">
<!-- <a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="setDefault(record)">
设置频次
</a-button>
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="viewTrack(record)">
</a-button> -->
<!-- <a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="viewTrack(record)">
日志
</a-button>
<a-button type="link" size="small" style="color: #1890ff; padding: 0 4px;" @click="viewTrack(record)">
查看轨迹
</a-button>
</a-button> -->
</div>
</template>
</template>
@@ -553,6 +553,24 @@
</div>
</a-modal>
<!-- 溯源二维码模态框 -->
<a-modal
:open="qrcodeVisible"
title="溯源二维码"
:footer="null"
width="450px"
@cancel="handleQRCodeCancel"
>
<div class="qrcode-modal-content">
<div class="qrcode-container">
<img v-if="qrcodeUrl" :src="qrcodeUrl" alt="溯源二维码" class="qrcode-image" />
</div>
<div class="device-sn-display">
{{ currentDeviceSn }}
</div>
</div>
</a-modal>
<!-- 添加/编辑模态框 -->
<a-modal
:open="modalVisible"
@@ -613,6 +631,7 @@ import { PlusOutlined, ReloadOutlined, ExportOutlined, EnvironmentOutlined, Crow
import { api, directApi } from '../utils/api'
import { ExportUtils } from '../utils/exportUtils'
import { loadBMapScript, createMap } from '@/utils/mapService'
import QRCode from 'qrcode'
// 响应式数据
const collars = ref([])
@@ -643,6 +662,11 @@ const currentLocation = ref(null)
const baiduMap = ref(null)
const locationMarker = ref(null)
// 二维码相关数据
const qrcodeVisible = ref(false)
const qrcodeUrl = ref('')
const currentDeviceSn = ref('')
// 标签页配置
const tabs = [
{ key: 'identity', label: '身份信息' },
@@ -1013,8 +1037,30 @@ const editDevice = (record) => {
message.info(`修改设备 ${record.deviceId}`)
}
const clearData = (record) => {
message.info(`清除设备 ${record.deviceId} 的数据`)
// 显示溯源二维码
const clearData = async (record) => {
try {
const deviceSn = record.sn
if (!deviceSn) {
message.warning('设备编号不存在')
return
}
currentDeviceSn.value = deviceSn
const traceUrl = `https://farm.aiotagro.com/source/source-index.html?device_sn=${deviceSn}`
// 生成二维码
const qrCodeDataUrl = await QRCode.toDataURL(traceUrl, {
width: 250,
margin: 2
})
qrcodeUrl.value = qrCodeDataUrl
qrcodeVisible.value = true
} catch (error) {
console.error('生成二维码失败:', error)
message.error('生成二维码失败,请重试')
}
}
// 搜索项圈
@@ -1334,6 +1380,13 @@ const handleLocationCancel = () => {
}
}
// 关闭二维码模态框
const handleQRCodeCancel = () => {
qrcodeVisible.value = false
qrcodeUrl.value = ''
currentDeviceSn.value = ''
}
// 初始化百度地图
const initBaiduMap = async () => {
if (!currentLocation.value) return
@@ -1773,4 +1826,37 @@ onUnmounted(() => {
border-top: 1px solid #f0f0f0;
margin-top: 20px;
}
/* 二维码模态框样式 */
.qrcode-modal-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.qrcode-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.qrcode-image {
width: 250px;
height: 250px;
border: 1px solid #e8e8e8;
border-radius: 4px;
}
.device-sn-display {
font-size: 16px;
font-weight: 500;
color: #333;
text-align: center;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
width: 100%;
}
</style>

View File

@@ -154,18 +154,18 @@
<!-- 操作列 -->
<template #action="{ record }">
<div class="action-buttons">
<a-button type="link" size="small" @click="bindLivestock(record)">
<!-- <a-button type="link" size="small" @click="bindLivestock(record)">
绑定牲畜
</a-button>
</a-button> -->
<a-button type="link" size="small" @click="showQRCode(record)">
溯源二维码
</a-button>
<a-button type="link" size="small" @click="showBindingInfo(record)">
绑定信息
</a-button>
<a-button type="link" size="small" @click="showLog(record)">
<!-- <a-button type="link" size="small" @click="showLog(record)">
日志
</a-button>
</a-button> -->
</div>
</template>
</a-table>
@@ -219,6 +219,24 @@
</div>
</a-modal>
<!-- 溯源二维码模态框 -->
<a-modal
:open="qrcodeVisible"
title="溯源二维码"
:footer="null"
width="450px"
@cancel="handleQRCodeCancel"
>
<div class="qrcode-modal-content">
<div class="qrcode-container">
<img v-if="qrcodeUrl" :src="qrcodeUrl" alt="溯源二维码" class="qrcode-image" />
</div>
<div class="device-sn-display">
{{ currentDeviceSn }}
</div>
</div>
</a-modal>
<!-- 绑定信息模态框 -->
<a-modal
:open="bindInfoVisible"
@@ -337,6 +355,7 @@ import { EnvironmentOutlined, SearchOutlined, ExportOutlined } from '@ant-design
import { api, directApi } from '../utils/api'
import { ExportUtils } from '../utils/exportUtils'
import { formatBindingInfo } from '../utils/fieldMappings'
import QRCode from 'qrcode'
// 响应式数据
const loading = ref(false)
@@ -362,6 +381,11 @@ const bindInfoData = ref({
const activeTab = ref('basic')
const currentEartagNumber = ref('')
// 二维码相关数据
const qrcodeVisible = ref(false)
const qrcodeUrl = ref('')
const currentDeviceSn = ref('')
// 分页配置
const pagination = reactive({
current: 1,
@@ -752,9 +776,30 @@ const bindLivestock = (record) => {
message.info('绑定牲畜功能开发中...')
}
// 显示二维码
const showQRCode = (record) => {
message.info('溯源二维码功能开发中...')
// 显示溯源二维码
const showQRCode = async (record) => {
try {
const deviceSn = record.eartagNumber
if (!deviceSn) {
message.warning('设备编号不存在')
return
}
currentDeviceSn.value = deviceSn
const traceUrl = `https://farm.aiotagro.com/source/source-index.html?device_sn=${deviceSn}`
// 生成二维码
const qrCodeDataUrl = await QRCode.toDataURL(traceUrl, {
width: 250,
margin: 2
})
qrcodeUrl.value = qrCodeDataUrl
qrcodeVisible.value = true
} catch (error) {
console.error('生成二维码失败:', error)
message.error('生成二维码失败,请重试')
}
}
// 关闭绑定信息模态框
@@ -773,6 +818,13 @@ const handleBindingInfoCancel = () => {
activeTab.value = 'basic'
}
// 关闭二维码模态框
const handleQRCodeCancel = () => {
qrcodeVisible.value = false
qrcodeUrl.value = ''
currentDeviceSn.value = ''
}
// 显示绑定信息
const showBindingInfo = async (record) => {
try {
@@ -1238,6 +1290,39 @@ onUnmounted(() => {
font-weight: 500;
}
/* 二维码模态框样式 */
.qrcode-modal-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.qrcode-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.qrcode-image {
width: 250px;
height: 250px;
border: 1px solid #e8e8e8;
border-radius: 4px;
}
.device-sn-display {
font-size: 16px;
font-weight: 500;
color: #333;
text-align: center;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
width: 100%;
}
@media (max-width: 768px) {
.search-bar .ant-col {
margin-bottom: 12px;

View File

@@ -96,9 +96,9 @@
<a-button type="link" class="action-link" @click="viewLocation(record)">
查看定位
</a-button>
<a-button type="link" class="action-link" @click="viewCollectionInfo(record)">
<!-- <a-button type="link" class="action-link" @click="viewCollectionInfo(record)">
查看采集信息
</a-button>
</a-button> -->
</div>
</template>
</template>

View File

@@ -1,195 +0,0 @@
# 智能项圈预警检测逻辑说明
## 概述
为智能项圈预警系统添加了自动预警检测逻辑,根据设备数据自动判断预警类型,无需依赖后端预处理的预警数据。
## 预警检测规则
### 1. 低电量预警
- **条件**: `battery < 20`
- **级别**: 高级
- **颜色**: 橙色
- **说明**: 当设备电量低于20%时触发
### 2. 离线预警
- **条件**: `is_connect === 0`
- **级别**: 高级
- **颜色**: 红色
- **说明**: 当设备连接状态为0时触发
### 3. 佩戴异常预警
- **条件**: `bandge_status === 0`
- **级别**: 中级
- **颜色**: 蓝色
- **说明**: 当设备佩戴状态为0时触发
### 4. 温度过低预警
- **条件**: `temperature < 20`
- **级别**: 中级
- **颜色**: 蓝色
- **说明**: 当设备温度低于20°C时触发
### 5. 温度过高预警
- **条件**: `temperature > 40`
- **级别**: 高级
- **颜色**: 红色
- **说明**: 当设备温度高于40°C时触发
### 6. 异常运动预警
- **条件**: `steps - y_steps === 0`
- **级别**: 中级
- **颜色**: 紫色
- **说明**: 当当日步数与昨日步数相同时触发
## 实现细节
### 判断函数
```javascript
const determineAlertType = (record) => {
const alerts = []
// 检查电量预警
if (record.battery !== undefined && record.battery !== null && record.battery < 20) {
alerts.push('battery')
}
// 检查脱落预警 (bandge_status为0)
if (record.bandge_status !== undefined && record.bandge_status !== null && record.bandge_status === 0) {
alerts.push('wear')
}
// 检查离线预警 (is_connect为0)
if (record.is_connect !== undefined && record.is_connect !== null && record.is_connect === 0) {
alerts.push('offline')
}
// 检查温度预警
if (record.temperature !== undefined && record.temperature !== null) {
if (record.temperature < 20) {
alerts.push('temperature_low')
} else if (record.temperature > 40) {
alerts.push('temperature_high')
}
}
// 检查运动异常预警 (steps - y_steps为0)
if (record.steps !== undefined && record.y_steps !== undefined &&
record.steps !== null && record.y_steps !== null) {
const movementDiff = record.steps - record.y_steps
if (movementDiff === 0) {
alerts.push('movement')
}
}
// 返回第一个预警类型如果没有预警则返回null
return alerts.length > 0 ? alerts[0] : null
}
```
### 预警级别分配
```javascript
let alertLevel = 'low'
if (determinedAlertType === 'battery' || determinedAlertType === 'offline' || determinedAlertType === 'temperature_high') {
alertLevel = 'high'
} else if (determinedAlertType === 'movement' || determinedAlertType === 'wear' || determinedAlertType === 'temperature_low') {
alertLevel = 'medium'
}
```
### 统计数据计算
```javascript
const calculateStats = (data) => {
const newStats = {
lowBattery: 0,
offline: 0,
highTemperature: 0,
abnormalMovement: 0,
wearOff: 0
}
data.forEach(item => {
const alertType = determineAlertType(item)
if (alertType === 'battery') {
newStats.lowBattery++
} else if (alertType === 'offline') {
newStats.offline++
} else if (alertType === 'temperature_high') {
newStats.highTemperature++
} else if (alertType === 'movement') {
newStats.abnormalMovement++
} else if (alertType === 'wear') {
newStats.wearOff++
}
})
return newStats
}
```
## 测试验证
### 运行测试脚本
```bash
cd backend
node test-alert-detection-logic.js
```
### 测试用例
测试脚本包含以下测试用例:
1. 正常设备
2. 低电量预警
3. 离线预警
4. 佩戴异常预警
5. 温度过低预警
6. 温度过高预警
7. 异常运动预警
8. 多重预警(低电量+离线)
9. 边界值测试电量20/19温度20/19/40/41
## 前端集成
### 数据转换
在数据转换过程中,系统会:
1. 调用 `determineAlertType()` 函数判断预警类型
2. 根据预警类型确定预警级别
3. 更新统计卡片数据
4. 在表格中显示相应的预警标签
### 筛选功能
筛选下拉菜单包含所有预警类型:
- 全部预警
- 低电量预警
- 离线预警
- 温度过低预警
- 温度过高预警
- 异常运动预警
- 佩戴异常预警
## 优势
1. **实时检测**: 基于最新设备数据实时判断预警
2. **灵活配置**: 预警规则可以轻松调整
3. **多重预警**: 支持检测多种预警类型
4. **优先级处理**: 当存在多重预警时,返回第一个检测到的预警
5. **边界值处理**: 精确处理边界值情况
6. **前端计算**: 减少后端计算负担,提高响应速度
## 注意事项
1. **数据完整性**: 确保设备数据包含所有必要字段
2. **空值处理**: 函数会检查字段是否存在且不为null
3. **类型转换**: 确保数值字段为数字类型
4. **性能考虑**: 大量数据时计算统计可能影响性能
5. **规则一致性**: 前后端预警规则应保持一致
## 相关文件
- `admin-system/src/views/SmartCollarAlert.vue` - 前端预警页面
- `backend/test-alert-detection-logic.js` - 测试脚本
- `backend/ALERT_DETECTION_LOGIC.md` - 本文档
---
**实现时间**: 2025-01-18
**版本**: v1.0.0
**状态**: 已实现并测试

View File

@@ -1,114 +0,0 @@
# API文档问题排查指南
## 问题描述
在访问 `http://localhost:5350/api-docs/` 时,找不到 `/api/smart-alerts/public` 相关的API接口。
## 解决方案
### 1. 确保服务器正确启动
```bash
cd backend
npm start
```
确保服务器在端口5350上启动。
### 2. 检查端口配置
确保 `server.js` 中的端口配置正确:
```javascript
const PORT = process.env.PORT || 5350;
```
### 3. 使用简化的Swagger配置
我已经创建了一个简化的Swagger配置文件 `swagger-simple.js`它手动定义了所有API路径。
### 4. 运行测试脚本
```bash
# 测试API访问
node test-api-access.js
# 启动服务器并测试
node start-and-test.js
```
### 5. 手动验证API
在浏览器中访问以下URL来验证API是否正常工作
- 根路径: http://localhost:5350/
- API文档: http://localhost:5350/api-docs/
- Swagger JSON: http://localhost:5350/api-docs/swagger.json
- 智能耳标预警统计: http://localhost:5350/api/smart-alerts/public/eartag/stats
- 智能项圈预警统计: http://localhost:5350/api/smart-alerts/public/collar/stats
### 6. 检查路由注册
确保在 `server.js` 中正确注册了智能预警路由:
```javascript
// 智能预警相关路由
app.use('/api/smart-alerts', require('./routes/smart-alerts'));
```
### 7. 重新启动服务器
如果修改了配置,请重新启动服务器:
```bash
# 停止当前服务器 (Ctrl+C)
# 然后重新启动
npm start
```
## 预期结果
正确配置后,在 `http://localhost:5350/api-docs/` 中应该能看到:
### 智能耳标预警 API
- `GET /smart-alerts/public/eartag/stats` - 获取预警统计
- `GET /smart-alerts/public/eartag` - 获取预警列表
- `GET /smart-alerts/public/eartag/{id}` - 获取预警详情
- `POST /smart-alerts/public/eartag/{id}/handle` - 处理预警
- `POST /smart-alerts/public/eartag/batch-handle` - 批量处理预警
- `GET /smart-alerts/public/eartag/export` - 导出预警数据
### 智能项圈预警 API
- `GET /smart-alerts/public/collar/stats` - 获取预警统计
- `GET /smart-alerts/public/collar` - 获取预警列表
- `GET /smart-alerts/public/collar/{id}` - 获取预警详情
- `POST /smart-alerts/public/collar/{id}/handle` - 处理预警
- `POST /smart-alerts/public/collar/batch-handle` - 批量处理预警
- `GET /smart-alerts/public/collar/export` - 导出预警数据
## 故障排除
### 如果仍然看不到API路径
1. **检查控制台错误**:查看服务器启动时的错误信息
2. **检查依赖**:确保安装了 `swagger-jsdoc``swagger-ui-express`
3. **检查文件路径**:确保所有文件都在正确的位置
4. **清除缓存**:重启浏览器或清除缓存
### 如果API调用失败
1. **检查数据库连接**:确保数据库服务正在运行
2. **检查模型导入**:确保所有模型都正确导入
3. **查看服务器日志**:检查具体的错误信息
## 联系支持
如果问题仍然存在,请提供以下信息:
1. 服务器启动日志
2. 浏览器控制台错误
3. 具体的错误信息
4. 操作系统和Node.js版本
---
**注意**: 确保在运行任何测试之前,数据库服务正在运行,并且所有必要的依赖都已安装。

View File

@@ -1,394 +0,0 @@
# 智能耳标预警 API 接口文档
## 概述
智能耳标预警系统提供了完整的API接口支持预警数据的查询、统计、处理和导出功能。所有接口均为公开接口无需身份验证。
## 基础信息
- **基础URL**: `http://localhost:5350/api/smart-alerts/public`
- **数据格式**: JSON
- **字符编码**: UTF-8
## 接口列表
### 1. 获取智能耳标预警统计
**接口地址**: `GET /eartag/stats`
**功能描述**: 获取智能耳标预警的统计数据,包括各类预警的数量和设备总数。
**请求参数**: 无
**响应示例**:
```json
{
"success": true,
"data": {
"totalDevices": 150,
"lowBattery": 12,
"offline": 8,
"highTemperature": 5,
"lowTemperature": 3,
"abnormalMovement": 7,
"totalAlerts": 35
},
"message": "获取智能耳标预警统计成功"
}
```
**响应字段说明**:
- `totalDevices`: 耳标设备总数
- `lowBattery`: 低电量预警数量
- `offline`: 离线预警数量
- `highTemperature`: 高温预警数量
- `lowTemperature`: 低温预警数量
- `abnormalMovement`: 异常运动预警数量
- `totalAlerts`: 预警总数
### 2. 获取智能耳标预警列表
**接口地址**: `GET /eartag`
**功能描述**: 获取智能耳标预警列表,支持分页、搜索和筛选。
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| page | number | 否 | 1 | 页码 |
| limit | number | 否 | 10 | 每页数量 |
| search | string | 否 | - | 搜索关键词(耳标编号) |
| alertType | string | 否 | - | 预警类型筛选 |
| alertLevel | string | 否 | - | 预警级别筛选 |
| status | string | 否 | - | 设备状态筛选 |
| startDate | string | 否 | - | 开始日期 (YYYY-MM-DD) |
| endDate | string | 否 | - | 结束日期 (YYYY-MM-DD) |
**预警类型枚举**:
- `battery`: 低电量预警
- `offline`: 离线预警
- `temperature`: 温度预警
- `movement`: 异常运动预警
**预警级别枚举**:
- `high`: 高级
- `medium`: 中级
- `low`: 低级
**设备状态枚举**:
- `online`: 在线
- `offline`: 离线
- `alarm`: 报警
- `maintenance`: 维护
**响应示例**:
```json
{
"success": true,
"data": [
{
"id": "123_offline",
"deviceId": 123,
"deviceName": "EARTAG001",
"eartagNumber": "EARTAG001",
"alertType": "offline",
"alertLevel": "high",
"alertTime": "2024-01-15 10:30:00",
"battery": 85,
"temperature": 25.5,
"dailySteps": 0,
"totalSteps": 1500,
"yesterdaySteps": 1500,
"deviceStatus": "离线",
"gpsSignal": "无",
"movementStatus": "静止",
"description": "设备已离线超过30分钟",
"longitude": 116.3974,
"latitude": 39.9093
}
],
"total": 35,
"stats": {
"lowBattery": 12,
"offline": 8,
"highTemperature": 5,
"abnormalMovement": 7
},
"pagination": {
"page": 1,
"limit": 10,
"total": 35,
"pages": 4
},
"message": "获取智能耳标预警列表成功"
}
```
### 3. 获取单个智能耳标预警详情
**接口地址**: `GET /eartag/{id}`
**功能描述**: 获取指定ID的智能耳标预警详细信息。
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | string | 是 | 预警ID格式为 deviceId_alertType |
**响应示例**:
```json
{
"success": true,
"data": {
"id": "123_offline",
"deviceId": 123,
"deviceName": "EARTAG001",
"eartagNumber": "EARTAG001",
"alertType": "offline",
"alertLevel": "high",
"alertTime": "2024-01-15 10:30:00",
"battery": 85,
"temperature": 25.5,
"dailySteps": 0,
"totalSteps": 1500,
"yesterdaySteps": 1500,
"deviceStatus": "离线",
"gpsSignal": "无",
"movementStatus": "静止",
"description": "设备已离线超过30分钟",
"longitude": 116.3974,
"latitude": 39.9093
},
"message": "获取智能耳标预警详情成功"
}
```
### 4. 处理智能耳标预警
**接口地址**: `POST /eartag/{id}/handle`
**功能描述**: 处理指定的智能耳标预警。
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | string | 是 | 预警ID |
**请求体**:
```json
{
"action": "acknowledged",
"notes": "已联系技术人员处理",
"handler": "张三"
}
```
**请求字段说明**:
- `action`: 处理动作(可选,默认为 "acknowledged"
- `notes`: 处理备注(可选)
- `handler`: 处理人(可选,默认为 "system"
**响应示例**:
```json
{
"success": true,
"data": {
"alertId": "123_offline",
"action": "acknowledged",
"notes": "已联系技术人员处理",
"handler": "张三",
"processedAt": "2024-01-15T10:35:00.000Z",
"status": "processed"
},
"message": "预警处理成功"
}
```
### 5. 批量处理智能耳标预警
**接口地址**: `POST /eartag/batch-handle`
**功能描述**: 批量处理多个智能耳标预警。
**请求体**:
```json
{
"alertIds": ["123_offline", "124_battery", "125_temperature"],
"action": "acknowledged",
"notes": "批量处理完成",
"handler": "李四"
}
```
**请求字段说明**:
- `alertIds`: 预警ID列表必填
- `action`: 处理动作(可选,默认为 "acknowledged"
- `notes`: 处理备注(可选)
- `handler`: 处理人(可选,默认为 "system"
**响应示例**:
```json
{
"success": true,
"data": {
"processedCount": 3,
"results": [
{
"alertId": "123_offline",
"action": "acknowledged",
"notes": "批量处理完成",
"handler": "李四",
"processedAt": "2024-01-15T10:35:00.000Z",
"status": "processed"
}
]
},
"message": "成功处理 3 个预警"
}
```
### 6. 导出智能耳标预警数据
**接口地址**: `GET /eartag/export`
**功能描述**: 导出智能耳标预警数据支持JSON和CSV格式。
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| search | string | 否 | - | 搜索关键词 |
| alertType | string | 否 | - | 预警类型筛选 |
| alertLevel | string | 否 | - | 预警级别筛选 |
| startDate | string | 否 | - | 开始日期 |
| endDate | string | 否 | - | 结束日期 |
| format | string | 否 | json | 导出格式 (json/csv) |
**响应示例** (JSON格式):
```json
{
"success": true,
"data": [
{
"id": "123_offline",
"deviceId": 123,
"deviceName": "EARTAG001",
"eartagNumber": "EARTAG001",
"alertType": "offline",
"alertLevel": "high",
"alertTime": "2024-01-15 10:30:00",
"battery": 85,
"temperature": 25.5,
"dailySteps": 0,
"totalSteps": 1500,
"yesterdaySteps": 1500,
"deviceStatus": "离线",
"gpsSignal": "无",
"movementStatus": "静止",
"description": "设备已离线超过30分钟",
"longitude": 116.3974,
"latitude": 39.9093
}
],
"total": 35,
"message": "导出智能耳标预警数据成功"
}
```
## 错误处理
所有接口在发生错误时都会返回统一的错误格式:
```json
{
"success": false,
"message": "错误描述",
"error": "详细错误信息"
}
```
**常见HTTP状态码**:
- `200`: 请求成功
- `400`: 请求参数错误
- `404`: 资源不存在
- `500`: 服务器内部错误
## 使用示例
### JavaScript/Node.js 示例
```javascript
// 获取预警统计
const stats = await fetch('http://localhost:3000/api/smart-alerts/public/eartag/stats')
.then(response => response.json());
// 获取预警列表
const alerts = await fetch('http://localhost:3000/api/smart-alerts/public/eartag?page=1&limit=10&alertType=battery')
.then(response => response.json());
// 处理预警
const result = await fetch('http://localhost:3000/api/smart-alerts/public/eartag/123_offline/handle', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'acknowledged',
notes: '已处理',
handler: '张三'
})
}).then(response => response.json());
```
### Python 示例
```python
import requests
import json
# 获取预警统计
stats_response = requests.get('http://localhost:3000/api/smart-alerts/public/eartag/stats')
stats = stats_response.json()
# 获取预警列表
alerts_response = requests.get('http://localhost:3000/api/smart-alerts/public/eartag',
params={'page': 1, 'limit': 10, 'alertType': 'battery'})
alerts = alerts_response.json()
# 处理预警
handle_data = {
'action': 'acknowledged',
'notes': '已处理',
'handler': '张三'
}
result = requests.post('http://localhost:3000/api/smart-alerts/public/eartag/123_offline/handle',
json=handle_data)
```
### cURL 示例
```bash
# 获取预警统计
curl -X GET "http://localhost:3000/api/smart-alerts/public/eartag/stats"
# 获取预警列表
curl -X GET "http://localhost:3000/api/smart-alerts/public/eartag?page=1&limit=10&alertType=battery"
# 处理预警
curl -X POST "http://localhost:3000/api/smart-alerts/public/eartag/123_offline/handle" \
-H "Content-Type: application/json" \
-d '{"action": "acknowledged", "notes": "已处理", "handler": "张三"}'
```
## 注意事项
1. 所有接口均为公开接口,无需身份验证
2. 预警ID格式为 `deviceId_alertType`,例如 `123_offline`
3. 时间格式统一使用 ISO 8601 标准
4. 分页从1开始不是从0开始
5. 搜索功能支持模糊匹配
6. 导出功能支持大量数据,建议合理设置筛选条件
7. 批量处理功能有数量限制建议单次处理不超过100个预警
## 更新日志
- **v1.0.0** (2024-01-15): 初始版本,包含基础预警查询、统计、处理和导出功能

View File

@@ -1,88 +0,0 @@
# 数据一致性检查报告
## 问题描述
用户反映项圈编号22012000107在页面中显示的电量为14但数据库中显示的电量为98。
## 检查结果
### 数据库实际数据
通过直接查询数据库 `iot_xq_client`项圈22012000107的实际数据为
- **ID**: 3517
- **SN**: 22012000107
- **电量**: 14
- **温度**: 10.1
- **状态**: 0 (离线)
- **更新时间**: 1668348374 (2022-11-13)
### API返回数据
通过API `/api/smart-alerts/public/collar` 返回的数据:
- **项圈编号**: 22012000107
- **电量**: 14
- **温度**: 10.1
- **预警类型**: 低电量预警、离线预警、温度过低预警、异常运动预警、佩戴异常预警
### 前端显示数据
页面中显示的数据:
- **项圈编号**: 22012000107
- **电量**: 14
- **温度**: 10.1
- **预警类型**: 低电量预警
- **预警级别**: 中级
## 结论
**数据是一致的!** 项圈22012000107在数据库、API和前端页面中显示的电量都是14没有数据不一致的问题。
### 可能的原因
1. **查看的是不同项圈**: 用户可能查看的是其他项圈编号的数据
2. **时间差异**: 数据可能在不同时间点发生了变化
3. **缓存问题**: 可能存在浏览器缓存或应用缓存问题
4. **多个记录**: 可能存在多个相同项圈编号的记录
### 其他项圈的电量数据
从数据库查询结果可以看到,其他项圈的电量数据:
- 22012000108: 98%
- 15010000006: 98%
- 15010000007: 98%
- 15010000008: 83%
- 15010000015: 98%
## 建议
1. **确认项圈编号**: 请确认查看的是正确的项圈编号22012000107
2. **清除缓存**: 清除浏览器缓存并刷新页面
3. **检查时间**: 确认查看数据的时间点
4. **重新查询**: 重新查询数据库确认当前数据
## 技术细节
### 数据库表结构
- 表名: `iot_xq_client`
- 主键: `id`
- 项圈编号字段: `sn`
- 电量字段: `battery` (varchar类型)
- 温度字段: `temperature` (varchar类型)
### API处理流程
1. 从数据库查询设备数据
2. 根据数据生成预警信息
3. 返回包含原始数据的预警列表
4. 前端接收并显示数据
### 数据转换
- 数据库电量: "14" (字符串)
- API返回电量: 14 (数字)
- 前端显示电量: 14
## 相关文件
- `backend/check-specific-collar.js` - 特定项圈检查脚本
- `backend/check-database-data.js` - 数据库数据检查脚本
- `backend/check-all-tables.js` - 所有表检查脚本
- `backend/simple-db-test.js` - 简单数据库测试脚本
---
**检查时间**: 2025-01-18
**检查结果**: 数据一致,无问题
**状态**: 已确认

View File

@@ -1,120 +0,0 @@
# 智能项圈预警错误修复总结
## 问题描述
在智能项圈预警页面中出现了JavaScript错误
```
ReferenceError: determinedAlertType is not defined
at SmartCollarAlert.vue:611:32
```
## 问题原因
在数据转换的`map`函数中,`determinedAlertType`变量只在`if-else`块内部定义,但在函数外部被引用,导致作用域错误。
## 解决方案
`determinedAlertType`变量声明移到`if-else`块外部,确保在整个函数作用域内都可以访问。
### 修复前的问题代码
```javascript
if (item.alertType) {
// 使用API返回的预警类型
alertTypeText = alertTypeMap[item.alertType] || item.alertType
// determinedAlertType 在这里没有定义
} else {
// 如果没有预警类型,使用判断函数
const determinedAlertType = determineAlertType(item) // 只在else块中定义
alertTypeText = determinedAlertType ? getAlertTypeText(determinedAlertType) : '正常'
}
// 在函数外部引用 determinedAlertType - 这里会报错
determinedAlertType: determinedAlertType
```
### 修复后的代码
```javascript
let alertTypeText = '正常'
let alertLevel = 'low'
let determinedAlertType = null // 在外部声明
if (item.alertType) {
// 使用API返回的预警类型
alertTypeText = alertTypeMap[item.alertType] || item.alertType
determinedAlertType = item.alertType // 赋值
} else {
// 如果没有预警类型,使用判断函数
determinedAlertType = determineAlertType(item) // 重新赋值
alertTypeText = determinedAlertType ? getAlertTypeText(determinedAlertType) : '正常'
}
// 现在可以安全引用 determinedAlertType
determinedAlertType: determinedAlertType
```
## 修复效果
### 修复前
- ❌ JavaScript运行时错误
- ❌ 页面无法正常加载数据
- ❌ 控制台显示ReferenceError
### 修复后
- ✅ 无JavaScript错误
- ✅ 页面正常加载和显示数据
- ✅ 统计数据正确显示(非零值)
- ✅ 预警列表正常显示
## 测试验证
### 运行测试脚本
```bash
node backend/test-error-fix.js
```
### 测试结果
```
✅ API调用成功
数据条数: 3
统计数据: {
lowBattery: 22,
offline: 1779,
highTemperature: 1302,
abnormalMovement: 1908,
wearOff: 50
}
✅ 数据转换测试通过没有ReferenceError
```
## 当前功能状态
### 统计数据
- 低电量预警: 22个
- 离线预警: 1779个
- 温度预警: 1302个
- 异常运动预警: 1908个
- 佩戴异常预警: 50个
### 数据转换
- ✅ 正确使用API返回的预警类型
- ✅ 正确映射预警级别
- ✅ 保留判断函数作为备用
- ✅ 无JavaScript错误
### 页面功能
- ✅ 统计卡片显示
- ✅ 预警列表显示
- ✅ 搜索和筛选
- ✅ 分页功能
- ✅ 预警处理
- ✅ 数据导出
## 相关文件
- `admin-system/src/views/SmartCollarAlert.vue` - 主要修复文件
- `backend/test-error-fix.js` - 测试脚本
- `backend/ERROR_FIX_SUMMARY.md` - 本文档
---
**修复时间**: 2025-01-18
**修复版本**: v1.0.1
**状态**: 已修复并测试通过

View File

@@ -1,139 +0,0 @@
# 网络访问问题解决方案
## 问题描述
您能访问 `http://172.28.112.1:5300/` 而别人访问不了,这是因为网络配置和防火墙设置的问题。
## 已完成的修复
### 1. ✅ 后端服务器配置修复
- 修改了 `server.js` 文件
- 服务器现在监听所有网络接口 (`0.0.0.0:5350`)
- 不再只监听本地回环地址
### 2. ✅ 前端服务器配置检查
- 前端已正确配置 `host: '0.0.0.0'`
- 可以接受来自任何IP地址的连接
## 解决步骤
### 步骤1重启服务器
```bash
# 停止当前服务器Ctrl+C
# 重新启动后端服务器
cd backend
npm start
```
### 步骤2配置Windows防火墙
#### 方法A使用批处理脚本推荐
1. 右键点击 `configure-firewall.bat`
2. 选择"以管理员身份运行"
3. 等待配置完成
#### 方法B使用PowerShell脚本
1. 右键点击 `configure-firewall.ps1`
2. 选择"以管理员身份运行"
3. 等待配置完成
#### 方法C手动配置
以管理员身份运行PowerShell执行以下命令
```powershell
# 允许前端端口
netsh advfirewall firewall add rule name="Node.js Frontend Port 5300" dir=in action=allow protocol=TCP localport=5300
# 允许后端端口
netsh advfirewall firewall add rule name="Node.js Backend Port 5350" dir=in action=allow protocol=TCP localport=5350
```
### 步骤3验证配置
运行网络诊断脚本:
```bash
node fix-network-access.js
```
### 步骤4测试访问
让其他用户访问以下地址之一:
- `http://172.28.112.1:5300` (前端)
- `http://172.28.112.1:5350` (后端)
- `http://192.168.0.48:5300` (如果使用以太网)
- `http://192.168.0.48:5350` (如果使用以太网)
## 网络接口说明
根据诊断结果,您有以下可用的网络接口:
- **Clash**: 198.18.0.1 (VPN接口)
- **vEthernet (Default Switch)**: 172.28.112.1 (Hyper-V接口)
- **以太网**: 192.168.0.48 (主要网络接口)
- **VMware Network Adapter VMnet1**: 192.168.134.1
- **VMware Network Adapter VMnet8**: 192.168.238.1
## 常见问题解决
### 问题1其他用户仍然无法访问
**解决方案**
1. 确认其他用户与您在同一个局域网内
2. 使用正确的IP地址不是localhost
3. 检查路由器是否阻止了设备间通信
### 问题2端口被占用
**解决方案**
```bash
# 查看端口占用情况
netstat -ano | findstr :5300
netstat -ano | findstr :5350
# 终止占用端口的进程
taskkill /PID [进程ID] /F
```
### 问题3防火墙配置失败
**解决方案**
1. 确保以管理员身份运行脚本
2. 检查Windows防火墙是否被禁用
3. 手动在Windows安全中心添加规则
## 验证方法
### 1. 检查服务器状态
```bash
# 应该看到类似输出
netstat -ano | findstr :5300
# TCP 0.0.0.0:5300 0.0.0.0:0 LISTENING [PID]
netstat -ano | findstr :5350
# TCP 0.0.0.0:5350 0.0.0.0:0 LISTENING [PID]
```
### 2. 测试连接
```bash
# 测试本地连接
telnet localhost 5300
telnet localhost 5350
# 测试局域网连接
telnet 172.28.112.1 5300
telnet 172.28.112.1 5350
```
### 3. 浏览器测试
在浏览器中访问:
- `http://172.28.112.1:5300` - 应该看到前端页面
- `http://172.28.112.1:5350/api-docs` - 应该看到API文档
## 注意事项
1. **安全性**:开放端口到所有网络接口会降低安全性,仅用于开发环境
2. **网络环境**:确保在受信任的局域网环境中使用
3. **IP地址变化**如果IP地址发生变化需要更新访问地址
4. **路由器设置**:某些路由器可能阻止设备间通信,需要检查路由器设置
## 成功标志
当配置成功后,您应该看到:
- 服务器启动时显示"服务器监听所有网络接口 (0.0.0.0:5350)"
- 其他用户可以通过您的IP地址访问服务
- 防火墙规则已正确添加
- 网络诊断脚本显示端口可以正常监听
如果按照以上步骤操作后仍有问题,请检查网络环境或联系网络管理员。

View File

@@ -1,186 +0,0 @@
# ngrok外网访问配置指南
## 🎯 目标
让不在同一局域网的用户能够访问您的开发服务器
## 📋 完整步骤
### 步骤1注册ngrok账号
1. 访问 https://ngrok.com/
2. 点击 "Sign up" 注册免费账号
3. 验证邮箱后登录
### 步骤2获取认证令牌
1. 登录后访问 https://dashboard.ngrok.com/get-started/your-authtoken
2. 复制您的authtoken类似`2abc123def456ghi789jkl012mno345pqr678stu901vwx234yz`
### 步骤3配置ngrok认证
```bash
# 在backend目录下运行
.\ngrok.exe authtoken YOUR_AUTH_TOKEN
```
### 步骤4启动服务穿透
#### 方法A使用批处理脚本
```bash
# 双击运行
start-ngrok.bat
```
#### 方法B使用PowerShell脚本
```powershell
# 右键以管理员身份运行
.\start-ngrok.ps1
```
#### 方法C手动启动
```bash
# 启动前端穿透(新开一个终端窗口)
.\ngrok.exe http 5300
# 启动后端穿透(再开一个终端窗口)
.\ngrok.exe http 5350
```
### 步骤5获取访问地址
ngrok会显示类似这样的信息
```
Session Status online
Account your-email@example.com
Version 3.27.0
Region United States (us)
Latency 45ms
Web Interface http://127.0.0.1:4040
Forwarding https://abc123.ngrok.io -> http://localhost:5300
```
### 步骤6分享访问地址
- **前端访问地址**`https://abc123.ngrok.io`
- **后端访问地址**`https://def456.ngrok.io`
- **API文档地址**`https://def456.ngrok.io/api-docs`
## 🔧 高级配置
### 自定义子域名(付费功能)
```bash
# 使用自定义子域名
.\ngrok.exe http 5300 --subdomain=myapp-frontend
.\ngrok.exe http 5350 --subdomain=myapp-backend
```
### 同时启动多个服务
```bash
# 使用配置文件
.\ngrok.exe start --all --config=ngrok.yml
```
### 配置文件示例 (ngrok.yml)
```yaml
version: "2"
authtoken: YOUR_AUTH_TOKEN
tunnels:
frontend:
proto: http
addr: 5300
subdomain: myapp-frontend
backend:
proto: http
addr: 5350
subdomain: myapp-backend
```
## 📊 免费版限制
- 每次重启ngrokURL会变化
- 同时只能运行1个隧道
- 有连接数限制
- 有带宽限制
## 💰 付费版优势
- 固定子域名
- 多个隧道
- 更高带宽
- 更多功能
## 🚨 注意事项
1. **安全性**
- 外网访问会暴露您的服务
- 建议设置访问密码
- 不要在生产环境使用
2. **性能**
- 外网访问比内网慢
- 免费版有带宽限制
3. **稳定性**
- 免费版URL会变化
- 付费版更稳定
## 🛠️ 故障排除
### 问题1ngrok启动失败
```bash
# 检查网络连接
ping ngrok.com
# 重新配置认证
.\ngrok.exe authtoken YOUR_AUTH_TOKEN
```
### 问题2无法访问服务
```bash
# 检查本地服务是否运行
netstat -ano | findstr :5300
netstat -ano | findstr :5350
# 检查防火墙设置
netsh advfirewall firewall show rule name="Node.js Frontend Port 5300"
```
### 问题3URL无法访问
- 检查ngrok是否在线
- 重新启动ngrok
- 检查本地服务状态
## 📱 移动端访问
ngrok提供的HTTPS地址可以在移动设备上正常访问
- 手机浏览器访问:`https://abc123.ngrok.io`
- 平板电脑访问:`https://abc123.ngrok.io`
## 🔄 自动重启脚本
创建自动重启脚本当ngrok断开时自动重连
```bash
# auto-restart-ngrok.bat
@echo off
:start
echo 启动ngrok...
.\ngrok.exe http 5300
echo ngrok断开3秒后重新启动...
timeout /t 3 /nobreak >nul
goto start
```
## 📈 监控和日志
ngrok提供Web界面监控
- 访问http://127.0.0.1:4040
- 查看请求日志
- 监控连接状态
- 查看流量统计
## 🎉 完成!
配置完成后其他用户就可以通过ngrok提供的HTTPS地址访问您的开发服务器了
记住:
- 每次重启ngrokURL会变化
- 免费版有使用限制
- 建议在开发测试时使用

View File

@@ -1,341 +0,0 @@
# 智能预警系统 API 完整封装
## 概述
本项目为智能预警系统提供了完整的API接口封装包括智能耳标预警和智能项圈预警两个子系统。所有接口均为公开接口支持完整的CRUD操作、数据导出和实时监控功能。
## 系统架构
```
智能预警系统 API
├── 智能耳标预警 (Eartag Alerts)
│ ├── 预警统计
│ ├── 预警列表查询
│ ├── 预警详情获取
│ ├── 预警处理
│ ├── 批量处理
│ └── 数据导出
└── 智能项圈预警 (Collar Alerts)
├── 预警统计
├── 预警列表查询
├── 预警详情获取
├── 预警处理
├── 批量处理
└── 数据导出
```
## 功能特性
### ✅ 核心功能
- **预警统计**: 实时统计各类预警数量
- **预警查询**: 支持分页、搜索、多维度筛选
- **预警详情**: 获取单个预警的完整信息
- **预警处理**: 单个和批量预警处理
- **数据导出**: 支持JSON和CSV格式导出
- **实时监控**: 提供预警变化监控功能
### ✅ 技术特性
- **RESTful API**: 遵循REST设计原则
- **Swagger文档**: 完整的API文档和在线测试
- **错误处理**: 统一的错误响应格式
- **参数验证**: 完整的请求参数验证
- **性能优化**: 支持分页和筛选优化
- **跨平台**: 支持多种编程语言调用
## API接口总览
### 智能耳标预警 API
| 接口 | 方法 | 路径 | 功能 |
|------|------|------|------|
| 获取预警统计 | GET | `/eartag/stats` | 获取各类预警数量统计 |
| 获取预警列表 | GET | `/eartag` | 分页查询预警列表 |
| 获取预警详情 | GET | `/eartag/{id}` | 获取单个预警详情 |
| 处理预警 | POST | `/eartag/{id}/handle` | 处理单个预警 |
| 批量处理预警 | POST | `/eartag/batch-handle` | 批量处理预警 |
| 导出预警数据 | GET | `/eartag/export` | 导出预警数据 |
### 智能项圈预警 API
| 接口 | 方法 | 路径 | 功能 |
|------|------|------|------|
| 获取预警统计 | GET | `/collar/stats` | 获取各类预警数量统计 |
| 获取预警列表 | GET | `/collar` | 分页查询预警列表 |
| 获取预警详情 | GET | `/collar/{id}` | 获取单个预警详情 |
| 处理预警 | POST | `/collar/{id}/handle` | 处理单个预警 |
| 批量处理预警 | POST | `/collar/batch-handle` | 批量处理预警 |
| 导出预警数据 | GET | `/collar/export` | 导出预警数据 |
## 快速开始
### 1. 启动服务
```bash
cd backend
npm install
npm start
```
服务将在 `http://localhost:5350` 启动。
### 2. 访问API文档
打开浏览器访问:`http://localhost:5350/api-docs`
### 3. 测试API接口
```bash
# 测试智能耳标预警API
node test-smart-eartag-alert-api.js
# 测试智能项圈预警API
node test-smart-collar-alert-api.js
# 运行综合测试
node test-all-smart-alert-apis.js
```
## 使用示例
### Node.js 示例
```javascript
const axios = require('axios');
// 获取智能耳标预警统计
const eartagStats = await axios.get('http://localhost:5350/api/smart-alerts/public/eartag/stats');
console.log('耳标预警统计:', eartagStats.data);
// 获取智能项圈预警列表
const collarAlerts = await axios.get('http://localhost:5350/api/smart-alerts/public/collar?page=1&limit=10');
console.log('项圈预警列表:', collarAlerts.data);
// 处理预警
const handleResult = await axios.post('http://localhost:5350/api/smart-alerts/public/eartag/123_offline/handle', {
action: 'acknowledged',
notes: '已处理',
handler: '张三'
});
console.log('处理结果:', handleResult.data);
```
### 前端Vue示例
```javascript
import { smartAlertService } from '@/utils/dataService';
// 获取耳标预警列表
const eartagAlerts = await smartAlertService.getEartagAlerts({
page: 1,
limit: 10,
alertType: 'battery'
});
// 获取项圈预警统计
const collarStats = await smartAlertService.getCollarAlertStats();
// 批量处理预警
const batchResult = await smartAlertService.batchHandleEartagAlerts({
alertIds: ['123_offline', '124_battery'],
action: 'acknowledged',
notes: '批量处理',
handler: '管理员'
});
```
### Python 示例
```python
import requests
# 获取预警统计
response = requests.get('http://localhost:5350/api/smart-alerts/public/eartag/stats')
stats = response.json()
print('预警统计:', stats['data'])
# 处理预警
handle_data = {
'action': 'acknowledged',
'notes': '已处理',
'handler': '张三'
}
result = requests.post('http://localhost:5350/api/smart-alerts/public/eartag/123_offline/handle',
json=handle_data)
print('处理结果:', result.json())
```
## 数据模型
### 预警数据结构
#### 智能耳标预警
```javascript
{
"id": "123_offline", // 预警ID
"deviceId": 123, // 设备ID
"deviceName": "EARTAG001", // 设备名称
"eartagNumber": "EARTAG001", // 耳标编号
"alertType": "offline", // 预警类型
"alertLevel": "high", // 预警级别
"alertTime": "2024-01-15 10:30:00", // 预警时间
"battery": 85, // 设备电量
"temperature": 25.5, // 设备温度
"dailySteps": 0, // 当日步数
"deviceStatus": "离线", // 设备状态
"description": "设备已离线超过30分钟" // 预警描述
}
```
#### 智能项圈预警
```javascript
{
"id": "123_offline", // 预警ID
"deviceId": 123, // 设备ID
"deviceName": "COLLAR001", // 设备名称
"collarNumber": "COLLAR001", // 项圈编号
"alertType": "offline", // 预警类型
"alertLevel": "high", // 预警级别
"alertTime": "2024-01-15 10:30:00", // 预警时间
"battery": 85, // 设备电量
"temperature": 25.5, // 设备温度
"dailySteps": 0, // 当日步数
"deviceStatus": "离线", // 设备状态
"wearStatus": "未佩戴", // 佩戴状态
"description": "设备已离线超过30分钟" // 预警描述
}
```
### 预警类型
#### 智能耳标预警类型
- `battery`: 低电量预警
- `offline`: 离线预警
- `temperature`: 温度预警
- `movement`: 异常运动预警
#### 智能项圈预警类型
- `battery`: 低电量预警
- `offline`: 离线预警
- `temperature`: 温度预警
- `movement`: 异常运动预警
- `wear`: 项圈脱落预警
### 预警级别
- `high`: 高级
- `medium`: 中级
- `low`: 低级
## 配置说明
### 环境变量
```bash
PORT=5350 # API服务端口
NODE_ENV=development # 运行环境
```
### 数据库配置
系统使用MySQL数据库需要配置以下表
- `iot_jbq_client`: 智能耳标设备数据
- `iot_xq_client`: 智能项圈设备数据
## 监控和维护
### 日志记录
- API调用日志
- 错误日志
- 性能监控日志
### 监控指标
- API响应时间
- 错误率
- 请求量
- 预警处理效率
### 健康检查
```bash
# 检查服务状态
curl http://localhost:5350/
# 检查API文档
curl http://localhost:5350/api-docs/swagger.json
```
## 扩展开发
### 添加新的预警类型
1. 在控制器中添加新的预警检测逻辑
2. 更新API文档中的预警类型枚举
3. 更新前端界面的预警类型选项
### 添加新的处理动作
1. 在控制器中添加新的处理逻辑
2. 更新数据库模型(如果需要)
3. 更新API文档和前端界面
### 自定义筛选条件
1. 在控制器中添加新的筛选逻辑
2. 更新API文档中的参数说明
3. 更新前端界面的筛选选项
## 测试
### 运行测试
```bash
# 运行所有测试
npm test
# 运行特定测试
node test-smart-eartag-alert-api.js
node test-smart-collar-alert-api.js
node test-all-smart-alert-apis.js
```
### 测试覆盖
- ✅ API接口功能测试
- ✅ 参数验证测试
- ✅ 错误处理测试
- ✅ 数据格式验证测试
- ✅ 性能测试
- ✅ 并发测试
## 故障排除
### 常见问题
1. **API无法访问**
- 检查服务是否启动
- 检查端口是否正确
- 检查防火墙设置
2. **数据库连接失败**
- 检查数据库配置
- 检查数据库服务状态
- 检查网络连接
3. **API响应慢**
- 检查数据库性能
- 检查网络延迟
- 优化查询条件
### 调试模式
```bash
# 启用调试模式
DEBUG=* npm start
# 查看详细日志
NODE_ENV=development npm start
```
## 版本历史
- **v1.0.0** (2024-01-15): 初始版本包含完整的智能预警API系统
## 联系支持
如有问题或建议,请联系开发团队或查看项目文档。
---
**API文档地址**: http://localhost:5350/api-docs
**基础API地址**: http://localhost:5350/api/smart-alerts/public
**维护者**: 开发团队

View File

@@ -1,292 +0,0 @@
# 智能耳标预警 API 封装
## 概述
本项目为智能耳标预警系统提供了完整的API接口封装支持预警数据的查询、统计、处理和导出功能。所有接口均为公开接口其他程序可以轻松集成和调用。
## 功能特性
-**预警统计**: 获取各类预警的数量统计
-**预警查询**: 支持分页、搜索、筛选的预警列表查询
-**预警详情**: 获取单个预警的详细信息
-**预警处理**: 单个和批量预警处理功能
-**数据导出**: 支持JSON和CSV格式的数据导出
-**实时监控**: 提供预警变化监控功能
-**错误处理**: 完善的错误处理和响应机制
## 文件结构
```
backend/
├── controllers/
│ └── smartEartagAlertController.js # 智能耳标预警控制器
├── routes/
│ └── smart-alerts.js # 智能预警路由配置
├── examples/
│ └── smart-eartag-alert-usage.js # API使用示例
├── test-smart-eartag-alert-api.js # API测试脚本
├── API_INTEGRATION_GUIDE.md # API接口文档
└── README_SMART_EARTAG_ALERT_API.md # 本文件
admin-system/src/utils/
└── dataService.js # 前端数据服务封装
```
## 快速开始
### 1. 启动后端服务
```bash
cd backend
npm start
```
服务将在 `http://localhost:5350` 启动。
### 2. 测试API接口
```bash
# 运行API测试脚本
node test-smart-eartag-alert-api.js
```
### 3. 查看API文档
详细API文档请参考[API_INTEGRATION_GUIDE.md](./API_INTEGRATION_GUIDE.md)
## API接口列表
| 接口 | 方法 | 路径 | 功能 |
|------|------|------|------|
| 获取预警统计 | GET | `/eartag/stats` | 获取各类预警数量统计 |
| 获取预警列表 | GET | `/eartag` | 分页查询预警列表 |
| 获取预警详情 | GET | `/eartag/{id}` | 获取单个预警详情 |
| 处理预警 | POST | `/eartag/{id}/handle` | 处理单个预警 |
| 批量处理预警 | POST | `/eartag/batch-handle` | 批量处理预警 |
| 导出预警数据 | GET | `/eartag/export` | 导出预警数据 |
## 使用示例
### Node.js 示例
```javascript
const { SmartEartagAlertClient } = require('./examples/smart-eartag-alert-usage');
const client = new SmartEartagAlertClient();
// 获取预警统计
const stats = await client.getAlertStats();
console.log('预警统计:', stats.data);
// 获取预警列表
const alerts = await client.getAlerts({
page: 1,
limit: 10,
alertType: 'battery'
});
console.log('预警列表:', alerts.data);
// 处理预警
const result = await client.handleAlert('123_offline', {
action: 'acknowledged',
notes: '已处理',
handler: '张三'
});
console.log('处理结果:', result.data);
```
### 前端Vue示例
```javascript
import { smartAlertService } from '@/utils/dataService';
// 获取预警列表
const alerts = await smartAlertService.getEartagAlerts({
page: 1,
limit: 10,
alertType: 'battery'
});
// 处理预警
const result = await smartAlertService.handleEartagAlert(alertId, {
action: 'acknowledged',
notes: '已处理',
handler: '管理员'
});
```
### 监控预警变化
```javascript
const { AlertMonitor } = require('./examples/smart-eartag-alert-usage');
const monitor = new AlertMonitor(client, {
interval: 30000, // 30秒检查一次
onNewAlert: (count, stats) => {
console.log(`发现 ${count} 个新预警!`);
},
onAlertChange: (changes, stats) => {
console.log('预警统计变化:', changes);
}
});
monitor.start(); // 开始监控
```
## 数据模型
### 预警数据结构
```javascript
{
"id": "123_offline", // 预警ID
"deviceId": 123, // 设备ID
"deviceName": "EARTAG001", // 设备名称
"eartagNumber": "EARTAG001", // 耳标编号
"alertType": "offline", // 预警类型
"alertLevel": "high", // 预警级别
"alertTime": "2024-01-15 10:30:00", // 预警时间
"battery": 85, // 设备电量
"temperature": 25.5, // 设备温度
"dailySteps": 0, // 当日步数
"totalSteps": 1500, // 总步数
"yesterdaySteps": 1500, // 昨日步数
"deviceStatus": "离线", // 设备状态
"gpsSignal": "无", // GPS信号
"movementStatus": "静止", // 运动状态
"description": "设备已离线超过30分钟", // 预警描述
"longitude": 116.3974, // 经度
"latitude": 39.9093 // 纬度
}
```
### 预警类型
- `battery`: 低电量预警
- `offline`: 离线预警
- `temperature`: 温度预警
- `movement`: 异常运动预警
### 预警级别
- `high`: 高级
- `medium`: 中级
- `low`: 低级
## 配置说明
### 后端配置
确保后端服务在端口5350上运行
```javascript
// server.js
const PORT = process.env.PORT || 5350;
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});
```
### 前端配置
确保前端API基础URL配置正确
```javascript
// api.js
const API_BASE_URL = 'http://localhost:5350/api';
```
## 错误处理
所有API接口都遵循统一的错误响应格式
```javascript
{
"success": false,
"message": "错误描述",
"error": "详细错误信息"
}
```
常见HTTP状态码
- `200`: 请求成功
- `400`: 请求参数错误
- `404`: 资源不存在
- `500`: 服务器内部错误
## 性能优化
1. **分页查询**: 建议使用分页避免一次性加载大量数据
2. **筛选条件**: 使用筛选条件减少不必要的数据传输
3. **缓存机制**: 对于统计类数据可以考虑添加缓存
4. **批量操作**: 对于大量预警处理建议使用批量接口
## 扩展功能
### 添加新的预警类型
1. 在控制器中添加新的预警检测逻辑
2. 更新API文档中的预警类型枚举
3. 更新前端界面的预警类型选项
### 添加新的处理动作
1. 在控制器中添加新的处理逻辑
2. 更新数据库模型(如果需要)
3. 更新API文档和前端界面
## 测试
### 运行测试
```bash
# 运行完整测试套件
node test-smart-eartag-alert-api.js
# 运行使用示例
node examples/smart-eartag-alert-usage.js
```
### 测试覆盖
- ✅ API接口功能测试
- ✅ 参数验证测试
- ✅ 错误处理测试
- ✅ 数据格式验证测试
- ✅ 性能测试
## 维护说明
### 日志记录
所有API调用都会记录详细日志包括
- 请求参数
- 响应数据
- 错误信息
- 处理时间
### 监控指标
建议监控以下指标:
- API响应时间
- 错误率
- 请求量
- 预警处理效率
### 版本更新
API版本更新时请
1. 更新版本号
2. 更新API文档
3. 提供迁移指南
4. 保持向后兼容性
## 联系支持
如有问题或建议,请联系开发团队或查看项目文档。
---
**版本**: v1.0.0
**更新时间**: 2024-01-15
**维护者**: 开发团队

View File

@@ -0,0 +1,272 @@
# Node.js 服务管理脚本使用说明
宁夏智慧养殖监管平台后端服务管理脚本支持Linux和Windows系统。
## 文件说明
- **node_manager.sh** - Linux/CentOS服务器版本Bash脚本
- **node_manager.ps1** - Windows本地开发版本PowerShell脚本
## 一、Linux版本使用说明
### 1. 准备工作
在CentOS服务器上首次使用前需要设置脚本执行权限
```bash
chmod +x node_manager.sh
```
### 2. 基本命令
```bash
# 启动服务
./node_manager.sh start
# 停止服务
./node_manager.sh stop
# 重启服务
./node_manager.sh restart
# 查看服务状态
./node_manager.sh status
# 实时查看日志
./node_manager.sh logs
```
### 3. 完整部署流程示例
```bash
# 1. 进入项目目录
cd /path/to/backend
# 2. 安装依赖(首次部署)
npm install
# 3. 配置环境变量
cp env.example .env
vim .env # 编辑配置
# 4. 启动服务
./node_manager.sh start
# 5. 检查状态
./node_manager.sh status
# 6. 查看日志
./node_manager.sh logs
```
### 4. 常见问题
**Q: 提示 "端口已被占用" 怎么办?**
A: 使用 `./node_manager.sh stop` 停止服务,或手动查找并杀死进程:
```bash
# 查找占用端口的进程
lsof -ti:5350
# 杀死进程
kill -9 $(lsof -ti:5350)
```
**Q: 服务启动失败怎么办?**
A: 查看日志文件定位问题:
```bash
# 查看完整日志
cat logs/nxxmdata-backend.log
# 实时监控日志
tail -f logs/nxxmdata-backend.log
```
## 二、Windows版本使用说明
### 1. 准备工作
首次使用前可能需要设置PowerShell执行策略
```powershell
# 以管理员身份运行PowerShell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### 2. 基本命令
```powershell
# 启动服务
.\node_manager.ps1 start
# 停止服务
.\node_manager.ps1 stop
# 重启服务
.\node_manager.ps1 restart
# 查看服务状态
.\node_manager.ps1 status
# 实时查看日志
.\node_manager.ps1 logs
```
### 3. 完整部署流程示例
```powershell
# 1. 进入项目目录
cd D:\10-24\nxxmdata\backend
# 2. 安装依赖(首次部署)
npm install
# 3. 配置环境变量
Copy-Item env.example .env
notepad .env # 编辑配置
# 4. 启动服务
.\node_manager.ps1 start
# 5. 检查状态
.\node_manager.ps1 status
# 6. 查看日志
.\node_manager.ps1 logs
```
### 4. 常见问题
**Q: 提示 "端口已被占用" 怎么办?**
A: 使用脚本停止服务,或手动查找并杀死进程:
```powershell
# 查找占用端口的进程
Get-NetTCPConnection -LocalPort 5350 | Select-Object -ExpandProperty OwningProcess
# 杀死进程(替换 <PID> 为实际进程ID
Stop-Process -Id <PID> -Force
```
**Q: PowerShell提示无法执行脚本**
A: 设置执行策略:
```powershell
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
```
## 三、配置说明
### 应用配置
在脚本中已配置以下默认值:
| 配置项 | 值 | 说明 |
|--------|-----|------|
| APP_NAME | nxxmdata-backend | 应用名称 |
| ENTRY_FILE | server.js | 入口文件 |
| APP_PORT | 5350 | 监听端口 |
| NODE_ENV | production | 运行环境 |
| LOG_FILE | logs/nxxmdata-backend.log | 日志文件 |
### 修改配置
如需修改配置,编辑脚本文件的"配置区域"部分:
**Linux版本 (node_manager.sh):**
```bash
# 配置区域
APP_NAME="nxxmdata-backend"
ENTRY_FILE="server.js"
APP_PORT="5350"
```
**Windows版本 (node_manager.ps1):**
```powershell
# 配置区域
$APP_NAME = "nxxmdata-backend"
$ENTRY_FILE = "server.js"
$APP_PORT = 5350
```
## 四、生产环境建议
### 使用 PM2推荐
对于生产环境建议使用PM2进行进程管理
```bash
# 安装 PM2
npm install -g pm2
# 启动服务
pm2 start server.js --name nxxmdata-backend
# 保存配置
pm2 save
# 设置开机自启
pm2 startup
# 监控服务
pm2 monit
```
### 日志管理
定期清理日志文件,避免占用过多磁盘空间:
```bash
# Linux
find logs -name "*.log" -mtime +30 -delete
# Windows PowerShell
Get-ChildItem logs -Filter *.log | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-30)} | Remove-Item
```
## 五、脚本功能特点
### Linux版本特点
- ✅ 优雅停止进程SIGTERM → SIGKILL
- ✅ 端口占用检测
- ✅ 进程PID管理
- ✅ 彩色输出提示
- ✅ 详细的状态信息
- ✅ 自动创建日志目录
- ✅ 实时日志查看
### Windows版本特点
- ✅ PowerShell原生支持
- ✅ 端口占用检测
- ✅ 后台进程管理
- ✅ 彩色输出提示
- ✅ 详细的状态信息
- ✅ 自动创建日志目录
- ✅ 实时日志查看
## 六、技术支持
如遇到问题,请检查:
1. Node.js版本是否正确要求16.20.2
2. 依赖是否完整安装node_modules目录
3. 环境变量配置是否正确(.env文件
4. 端口是否被其他程序占用
5. 数据库连接是否正常
6. 日志文件中的错误信息
---
**项目信息:**
- 项目名称:宁夏智慧养殖监管平台
- 后端端口5350
- 前端端口5300
- API文档http://localhost:5350/api-docs

View File

@@ -1,127 +0,0 @@
# 智能项圈预警数据调用修复说明
## 问题描述
智能项圈预警页面 (`admin-system/src/views/SmartCollarAlert.vue`) 存在以下问题:
1. 统计数据使用硬编码,没有动态调用数据库
2. 部分功能没有正确调用API接口
3. 数据格式转换和显示存在问题
## 修复内容
### 1. 添加统计数据API调用
- 新增 `fetchStats()` 函数,专门用于获取统计数据
- 调用 `smartAlertService.getCollarAlertStats()` API
- 动态更新统计卡片数据(低电量、离线、温度、异常运动、佩戴异常)
### 2. 优化数据获取流程
- 修改 `fetchData()` 函数,专注于获取预警列表数据
- 在组件挂载时同时调用统计数据和列表数据API
- 在搜索、筛选、分页时同步更新统计数据
### 3. 完善API集成
- 更新 `handleAlert()` 函数,调用 `smartAlertService.handleCollarAlert()` API
- 更新 `exportData()` 函数,调用 `smartAlertService.exportCollarAlerts()` API
- 所有异步函数都使用 `async/await` 模式
### 4. 数据格式优化
- 保持原有的数据格式转换逻辑
- 确保API返回的数据能正确显示在界面上
- 优化错误处理和用户反馈
## 修改的文件
### admin-system/src/views/SmartCollarAlert.vue
主要修改:
1. 新增 `fetchStats()` 函数
2. 修改 `fetchData()` 函数,移除硬编码统计数据
3. 更新 `onMounted()` 钩子,同时获取统计和列表数据
4. 更新 `handleSearch()``handleFilterChange()``handleClearSearch()` 函数
5. 更新 `handleAlert()` 函数调用API处理预警
6. 更新 `exportData()` 函数调用API导出数据
## API端点使用
### 统计数据
- **端点**: `GET /api/smart-alerts/public/collar/stats`
- **功能**: 获取智能项圈预警统计数据
- **返回**: 各类预警的数量统计
### 预警列表
- **端点**: `GET /api/smart-alerts/public/collar`
- **功能**: 获取智能项圈预警列表
- **参数**: page, limit, search, alertType, alertLevel, status, startDate, endDate
### 预警详情
- **端点**: `GET /api/smart-alerts/public/collar/{id}`
- **功能**: 获取单个预警详情
### 处理预警
- **端点**: `POST /api/smart-alerts/public/collar/{id}/handle`
- **功能**: 处理指定的预警
### 批量处理预警
- **端点**: `POST /api/smart-alerts/public/collar/batch-handle`
- **功能**: 批量处理多个预警
### 导出数据
- **端点**: `GET /api/smart-alerts/public/collar/export`
- **功能**: 导出预警数据
- **参数**: format (json/csv), search, alertType, alertLevel, startDate, endDate
## 测试验证
### 运行测试脚本
```bash
cd backend
node test-smart-collar-alert-integration.js
```
### 前端页面验证
1. 打开智能项圈预警页面
2. 检查统计卡片是否显示动态数据
3. 测试搜索和筛选功能
4. 测试处理预警功能
5. 测试导出数据功能
## 预期效果
修复后的智能项圈预警页面应该能够:
1. **动态显示统计数据**
- 低电量预警数量
- 离线预警数量
- 温度预警数量
- 异常运动预警数量
- 佩戴异常预警数量
2. **完整的数据交互**
- 实时搜索和筛选
- 分页浏览
- 预警详情查看
- 预警处理操作
- 数据导出功能
3. **良好的用户体验**
- 加载状态提示
- 错误信息反馈
- 操作成功确认
## 注意事项
1. **确保后端服务运行**: 后端服务器必须在端口5350上运行
2. **数据库连接**: 确保数据库连接正常,相关表存在
3. **API权限**: 确保API端点可以正常访问
4. **数据格式**: 确保API返回的数据格式与前端期望一致
## 相关文件
- `backend/controllers/smartCollarAlertController.js` - 后端控制器
- `backend/routes/smart-alerts.js` - API路由定义
- `admin-system/src/utils/dataService.js` - 前端数据服务
- `backend/test-smart-collar-alert-integration.js` - 集成测试脚本
---
**修复完成时间**: 2025-01-18
**修复版本**: v1.0.0
**测试状态**: 待验证

View File

@@ -1,99 +0,0 @@
# ✅ 网络访问问题已解决
## 修复完成状态
### 1. ✅ 后端服务器配置
- **状态**: 已修复
- **配置**: 服务器现在监听 `0.0.0.0:5350`
- **验证**: `netstat` 显示 `TCP 0.0.0.0:5350 LISTENING`
### 2. ✅ 前端服务器配置
- **状态**: 已正确配置
- **配置**: 服务器监听 `0.0.0.0:5300`
- **验证**: `netstat` 显示 `TCP 0.0.0.0:5300 LISTENING`
### 3. ✅ Windows防火墙配置
- **状态**: 已配置
- **规则1**: Node.js Frontend Port 5300 (已启用)
- **规则2**: Node.js Backend Port 5350 (已启用)
- **验证**: 防火墙规则已确认添加
## 现在其他用户可以访问的地址
### 主要访问地址
- **前端**: `http://172.28.112.1:5300`
- **后端**: `http://172.28.112.1:5350`
- **API文档**: `http://172.28.112.1:5350/api-docs`
### 备用访问地址(如果主要地址不可用)
- **前端**: `http://192.168.0.48:5300`
- **后端**: `http://192.168.0.48:5350`
## 验证步骤
### 1. 本地验证
在您的浏览器中访问:
- `http://172.28.112.1:5300` - 应该看到前端页面
- `http://172.28.112.1:5350/api-docs` - 应该看到API文档
### 2. 外部用户验证
让其他用户在他们的浏览器中访问:
- `http://172.28.112.1:5300` - 应该看到前端页面
- `http://172.28.112.1:5350/api-docs` - 应该看到API文档
### 3. 网络连接测试
其他用户可以运行以下命令测试连接:
```cmd
# 测试前端端口
telnet 172.28.112.1 5300
# 测试后端端口
telnet 172.28.112.1 5350
```
## 故障排除
### 如果其他用户仍然无法访问:
1. **检查网络环境**
- 确保其他用户与您在同一个局域网内
- 确认没有使用VPN或代理
2. **检查IP地址**
- 使用 `ipconfig` 确认当前IP地址
- 如果IP地址发生变化更新访问地址
3. **检查防火墙**
- 确认Windows防火墙规则已启用
- 检查是否有其他安全软件阻止连接
4. **检查路由器设置**
- 某些路由器可能阻止设备间通信
- 检查路由器的访问控制设置
## 成功标志
当配置完全成功时,您应该看到:
- ✅ 服务器启动时显示"服务器监听所有网络接口"
- ✅ 防火墙规则已正确添加
- ✅ 其他用户可以通过IP地址访问服务
- ✅ 网络诊断脚本显示端口可以正常监听
## 注意事项
1. **安全性**: 当前配置允许局域网内所有设备访问,仅适用于开发环境
2. **IP地址**: 如果网络环境变化IP地址可能会改变
3. **端口占用**: 确保端口5300和5350没有被其他程序占用
4. **服务器状态**: 确保服务器持续运行
## 维护建议
1. **定期检查**: 定期运行 `node fix-network-access.js` 检查网络状态
2. **日志监控**: 查看服务器日志确认连接状态
3. **备份配置**: 保存防火墙配置以便快速恢复
---
**问题已完全解决!** 🎉
现在其他用户应该能够正常访问您的开发服务器了。如果还有任何问题,请检查上述故障排除步骤。

View File

@@ -1,98 +0,0 @@
/**
* 检查实际数据
* @file check-actual-data.js
* @description 检查数据库中项圈22012000107的实际数据
*/
const { sequelize } = require('./config/database-simple');
async function checkActualData() {
console.log('🔍 检查数据库中项圈22012000107的实际数据...\n');
try {
// 1. 测试数据库连接
console.log('1. 测试数据库连接...');
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 2. 查询项圈22012000107的数据
console.log('\n2. 查询项圈22012000107的数据...');
const [results] = await sequelize.query(`
SELECT
id, sn, deviceId, battery, temperature, state,
uptime, longitude, latitude, gps_state, rsrp,
bandge_status, is_connect, steps, y_steps
FROM iot_xq_client
WHERE sn = '22012000107'
ORDER BY uptime DESC
`);
console.log(`找到 ${results.length} 条记录`);
results.forEach((row, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', row.id);
console.log('SN:', row.sn);
console.log('设备ID:', row.deviceId);
console.log('电量:', row.battery, '(类型:', typeof row.battery, ')');
console.log('温度:', row.temperature, '(类型:', typeof row.temperature, ')');
console.log('状态:', row.state);
console.log('经度:', row.longitude);
console.log('纬度:', row.latitude);
console.log('GPS状态:', row.gps_state);
console.log('RSRP:', row.rsrp);
console.log('佩戴状态:', row.bandge_status);
console.log('连接状态:', row.is_connect);
console.log('步数:', row.steps);
console.log('昨日步数:', row.y_steps);
console.log('更新时间:', row.uptime);
});
// 3. 查询所有项圈的最新数据
console.log('\n3. 查询所有项圈的最新数据...');
const [allResults] = await sequelize.query(`
SELECT
id, sn, deviceId, battery, temperature, state, uptime
FROM iot_xq_client
ORDER BY uptime DESC
LIMIT 10
`);
console.log('所有项圈的最新数据:');
allResults.forEach((row, index) => {
console.log(`${index + 1}. SN: ${row.sn}, 电量: ${row.battery}, 温度: ${row.temperature}, 状态: ${row.state}`);
});
// 4. 检查数据库配置
console.log('\n4. 检查数据库配置...');
const config = sequelize.config;
console.log('数据库配置:');
console.log('主机:', config.host);
console.log('端口:', config.port);
console.log('数据库名:', config.database);
console.log('用户名:', config.username);
// 5. 检查当前数据库
console.log('\n5. 检查当前数据库...');
const [currentDb] = await sequelize.query('SELECT DATABASE() as current_db');
console.log('当前数据库:', currentDb[0].current_db);
// 6. 检查是否有其他数据库
console.log('\n6. 检查所有数据库...');
const [databases] = await sequelize.query('SHOW DATABASES');
console.log('所有数据库:');
databases.forEach(db => {
const dbName = Object.values(db)[0];
console.log('-', dbName);
});
} catch (error) {
console.error('❌ 检查失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行检查
checkActualData().catch(console.error);

View File

@@ -1,114 +0,0 @@
/**
* 检查所有相关表
* @file check-all-tables.js
* @description 检查所有可能包含项圈数据的表
*/
const { sequelize } = require('./config/database-simple');
async function checkAllTables() {
console.log('🔍 检查所有相关表...\n');
try {
// 1. 列出所有表
console.log('1. 列出所有表...');
const [tables] = await sequelize.query("SHOW TABLES");
console.log('数据库中的所有表:');
tables.forEach((table, index) => {
const tableName = Object.values(table)[0];
console.log(`${index + 1}. ${tableName}`);
});
// 2. 检查可能包含项圈数据的表
const possibleTables = [
'iot_xq_client',
'iot_collar',
'smart_collar',
'collar_device',
'device_info',
'iot_device'
];
console.log('\n2. 检查可能包含项圈数据的表...');
for (const tableName of possibleTables) {
try {
console.log(`\n检查表: ${tableName}`);
const [rows] = await sequelize.query(`SELECT COUNT(*) as count FROM ${tableName}`);
const count = rows[0].count;
console.log(`记录数: ${count}`);
if (count > 0) {
// 查看表结构
const [columns] = await sequelize.query(`DESCRIBE ${tableName}`);
console.log('表结构:');
columns.forEach(col => {
console.log(` ${col.Field}: ${col.Type}`);
});
// 查找包含22012000107的记录
const [searchResults] = await sequelize.query(`
SELECT * FROM ${tableName}
WHERE sn = '22012000107' OR device_id = '22012000107' OR deviceId = '22012000107'
LIMIT 5
`);
if (searchResults.length > 0) {
console.log(`找到 ${searchResults.length} 条包含22012000107的记录:`);
searchResults.forEach((row, index) => {
console.log(`记录${index + 1}:`, row);
});
} else {
console.log('未找到包含22012000107的记录');
}
}
} catch (error) {
console.log(`${tableName} 不存在或无法访问: ${error.message}`);
}
}
// 3. 检查iot_xq_client表的详细信息
console.log('\n3. 检查iot_xq_client表的详细信息...');
const [xqClientData] = await sequelize.query(`
SELECT * FROM iot_xq_client
WHERE sn = '22012000107'
ORDER BY uptime DESC
`);
console.log(`iot_xq_client表中项圈22012000107的记录:`);
xqClientData.forEach((row, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', row.id);
console.log('SN:', row.sn);
console.log('电量:', row.battery);
console.log('温度:', row.temperature);
console.log('状态:', row.state);
console.log('更新时间:', row.uptime);
console.log('创建时间:', row.created_at);
console.log('更新时间:', row.updated_at);
});
// 4. 检查是否有其他项圈编号
console.log('\n4. 检查所有项圈编号...');
const [allSnData] = await sequelize.query(`
SELECT sn, battery, temperature, state, uptime
FROM iot_xq_client
ORDER BY uptime DESC
LIMIT 20
`);
console.log('所有项圈编号及其电量:');
allSnData.forEach((row, index) => {
console.log(`${index + 1}. SN: ${row.sn}, 电量: ${row.battery}, 温度: ${row.temperature}, 状态: ${row.state}`);
});
} catch (error) {
console.error('❌ 检查失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行检查
checkAllTables().catch(console.error);

View File

@@ -1,99 +0,0 @@
/**
* 检查正确的数据库
* @file check-correct-database.js
* @description 使用正确的数据库配置检查项圈22012000107的数据
*/
const mysql = require('mysql2/promise');
async function checkCorrectDatabase() {
console.log('🔍 使用正确的数据库配置检查项圈22012000107的数据...\n');
try {
// 使用正确的数据库配置
const connection = await mysql.createConnection({
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'nxxmdata'
});
console.log('✅ 数据库连接成功');
// 查询项圈22012000107的数据
console.log('\n查询项圈22012000107的数据...');
const [results] = await connection.execute(`
SELECT
id, sn, deviceId, battery, temperature, state,
uptime, longitude, latitude, gps_state, rsrp,
bandge_status, is_connect, steps, y_steps
FROM iot_xq_client
WHERE sn = '22012000107'
ORDER BY uptime DESC
`);
console.log(`找到 ${results.length} 条记录`);
results.forEach((row, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', row.id);
console.log('SN:', row.sn);
console.log('设备ID:', row.deviceId);
console.log('电量:', row.battery, '(类型:', typeof row.battery, ')');
console.log('温度:', row.temperature, '(类型:', typeof row.temperature, ')');
console.log('状态:', row.state);
console.log('经度:', row.longitude);
console.log('纬度:', row.latitude);
console.log('GPS状态:', row.gps_state);
console.log('RSRP:', row.rsrp);
console.log('佩戴状态:', row.bandge_status);
console.log('连接状态:', row.is_connect);
console.log('步数:', row.steps);
console.log('昨日步数:', row.y_steps);
console.log('更新时间:', row.uptime);
});
// 查询所有项圈的最新数据
console.log('\n查询所有项圈的最新数据...');
const [allResults] = await connection.execute(`
SELECT
id, sn, deviceId, battery, temperature, state, uptime
FROM iot_xq_client
ORDER BY uptime DESC
LIMIT 20
`);
console.log('所有项圈的最新数据:');
allResults.forEach((row, index) => {
console.log(`${index + 1}. SN: ${row.sn}, 电量: ${row.battery}, 温度: ${row.temperature}, 状态: ${row.state}, 更新时间: ${row.uptime}`);
});
// 检查是否有电量为99的记录
console.log('\n检查是否有电量为99的记录...');
const [battery99Results] = await connection.execute(`
SELECT
id, sn, deviceId, battery, temperature, state, uptime
FROM iot_xq_client
WHERE battery = '99' OR battery = 99
ORDER BY uptime DESC
LIMIT 10
`);
console.log(`找到 ${battery99Results.length} 条电量为99的记录`);
battery99Results.forEach((row, index) => {
console.log(`${index + 1}. SN: ${row.sn}, 电量: ${row.battery}, 温度: ${row.temperature}, 状态: ${row.state}`);
});
await connection.end();
} catch (error) {
console.error('❌ 检查失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行检查
checkCorrectDatabase().catch(console.error);

View File

@@ -1,114 +0,0 @@
/**
* 检查数据库原始数据
* @file check-database-data.js
* @description 检查数据库中项圈22012000107的原始数据
*/
const { IotXqClient } = require('./models');
async function checkDatabaseData() {
console.log('🔍 检查数据库原始数据...\n');
try {
// 1. 查找项圈22012000107的所有记录
console.log('1. 查找项圈22012000107的所有记录...');
const devices = await IotXqClient.findAll({
where: {
sn: '22012000107'
},
order: [['uptime', 'DESC']]
});
console.log(`找到 ${devices.length} 条记录`);
devices.forEach((device, index) => {
console.log(`\n${index + 1}条记录:`);
console.log('ID:', device.id);
console.log('SN:', device.sn);
console.log('设备ID:', device.deviceId);
console.log('电量 (battery):', device.battery);
console.log('温度 (temperature):', device.temperature);
console.log('步数 (steps):', device.steps);
console.log('昨日步数 (y_steps):', device.y_steps);
console.log('状态 (state):', device.state);
console.log('佩戴状态 (bandge_status):', device.bandge_status);
console.log('更新时间 (uptime):', device.uptime);
console.log('创建时间 (createdAt):', device.createdAt);
console.log('更新时间 (updatedAt):', device.updatedAt);
});
// 2. 查找所有包含22012000107的记录
console.log('\n2. 查找所有包含22012000107的记录...');
const allDevices = await IotXqClient.findAll({
where: {
[require('sequelize').Op.or]: [
{ sn: '22012000107' },
{ deviceId: '22012000107' },
{ sn: { [require('sequelize').Op.like]: '%22012000107%' } }
]
},
order: [['uptime', 'DESC']]
});
console.log(`找到 ${allDevices.length} 条相关记录`);
allDevices.forEach((device, index) => {
console.log(`\n相关记录${index + 1}:`);
console.log('ID:', device.id);
console.log('SN:', device.sn);
console.log('设备ID:', device.deviceId);
console.log('电量:', device.battery);
console.log('温度:', device.temperature);
console.log('状态:', device.state);
console.log('更新时间:', device.uptime);
});
// 3. 检查最新的记录
console.log('\n3. 检查最新的记录...');
const latestDevice = await IotXqClient.findOne({
where: {
sn: '22012000107'
},
order: [['uptime', 'DESC']]
});
if (latestDevice) {
console.log('最新记录:');
console.log('电量:', latestDevice.battery);
console.log('温度:', latestDevice.temperature);
console.log('更新时间:', latestDevice.uptime);
} else {
console.log('未找到最新记录');
}
// 4. 检查是否有电量为98的记录
console.log('\n4. 检查是否有电量为98的记录...');
const battery98Devices = await IotXqClient.findAll({
where: {
battery: 98,
sn: '22012000107'
},
order: [['uptime', 'DESC']]
});
console.log(`找到 ${battery98Devices.length} 条电量为98的记录`);
battery98Devices.forEach((device, index) => {
console.log(`\n电量98记录${index + 1}:`);
console.log('ID:', device.id);
console.log('SN:', device.sn);
console.log('电量:', device.battery);
console.log('温度:', device.temperature);
console.log('更新时间:', device.uptime);
});
} catch (error) {
console.error('❌ 检查失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行检查
checkDatabaseData().catch(console.error);

View File

@@ -1,97 +0,0 @@
/**
* 检查其他数据库
* @file check-other-databases.js
* @description 检查其他数据库中是否有项圈22012000107的数据
*/
const mysql = require('mysql2/promise');
async function checkOtherDatabases() {
console.log('🔍 检查其他数据库中项圈22012000107的数据...\n');
const databases = ['nxxmdata', 'nxdata', 'datav', 'aipet_new'];
for (const dbName of databases) {
try {
console.log(`\n=== 检查数据库: ${dbName} ===`);
// 创建连接
const connection = await mysql.createConnection({
host: '192.168.0.240',
port: 3306,
user: 'root',
password: '', // 根据实际情况填写密码
database: dbName
});
// 查询项圈22012000107的数据
const [results] = await connection.execute(`
SELECT
id, sn, deviceId, battery, temperature, state,
uptime, longitude, latitude, gps_state, rsrp
FROM iot_xq_client
WHERE sn = '22012000107'
ORDER BY uptime DESC
LIMIT 5
`);
console.log(`找到 ${results.length} 条记录`);
results.forEach((row, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', row.id);
console.log('SN:', row.sn);
console.log('设备ID:', row.deviceId);
console.log('电量:', row.battery);
console.log('温度:', row.temperature);
console.log('状态:', row.state);
console.log('经度:', row.longitude);
console.log('纬度:', row.latitude);
console.log('GPS状态:', row.gps_state);
console.log('RSRP:', row.rsrp);
console.log('更新时间:', row.uptime);
});
await connection.end();
} catch (error) {
console.log(`❌ 数据库 ${dbName} 检查失败:`, error.message);
}
}
// 检查当前数据库的最新数据
console.log('\n=== 检查当前数据库的最新数据 ===');
try {
const connection = await mysql.createConnection({
host: '192.168.0.240',
port: 3306,
user: 'root',
password: '',
database: 'nxxmdata'
});
// 查询所有项圈的最新数据
const [allResults] = await connection.execute(`
SELECT
id, sn, deviceId, battery, temperature, state, uptime
FROM iot_xq_client
ORDER BY uptime DESC
LIMIT 20
`);
console.log('所有项圈的最新数据:');
allResults.forEach((row, index) => {
console.log(`${index + 1}. SN: ${row.sn}, 电量: ${row.battery}, 温度: ${row.temperature}, 状态: ${row.state}, 更新时间: ${row.uptime}`);
});
await connection.end();
} catch (error) {
console.log('❌ 查询当前数据库失败:', error.message);
}
process.exit(0);
}
// 运行检查
checkOtherDatabases().catch(console.error);

View File

@@ -1,63 +0,0 @@
/**
* 检查服务器配置
* @file check-server-config.js
* @description 检查服务器使用的数据库配置
*/
const { IotXqClient } = require('./models');
async function checkServerConfig() {
console.log('🔍 检查服务器配置...\n');
try {
// 1. 检查数据库配置
console.log('1. 检查数据库配置...');
const config = IotXqClient.sequelize.config;
console.log('数据库配置:');
console.log('主机:', config.host);
console.log('端口:', config.port);
console.log('数据库名:', config.database);
console.log('用户名:', config.username);
// 2. 测试连接
console.log('\n2. 测试数据库连接...');
await IotXqClient.sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 3. 查询项圈22012000107的数据
console.log('\n3. 查询项圈22012000107的数据...');
const devices = await IotXqClient.findAll({
where: {
sn: '22012000107'
},
order: [['uptime', 'DESC']]
});
console.log(`找到 ${devices.length} 条记录`);
devices.forEach((device, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', device.id);
console.log('SN:', device.sn);
console.log('电量:', device.battery);
console.log('温度:', device.temperature);
console.log('状态:', device.state);
console.log('更新时间:', device.uptime);
});
// 4. 检查环境变量
console.log('\n4. 检查环境变量...');
console.log('DB_HOST:', process.env.DB_HOST);
console.log('DB_PORT:', process.env.DB_PORT);
console.log('DB_PASSWORD:', process.env.DB_PASSWORD);
} catch (error) {
console.error('❌ 检查失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行检查
checkServerConfig().catch(console.error);

View File

@@ -1,61 +0,0 @@
/**
* 检查服务器环境变量
* @file check-server-env.js
* @description 检查服务器进程的环境变量
*/
const { spawn } = require('child_process');
// 启动服务器并检查环境变量
const server = spawn('node', ['server.js'], {
env: {
...process.env,
DB_HOST: '129.211.213.226',
DB_PORT: '9527',
DB_PASSWORD: 'aiotAiot123!'
},
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
server.stdout.on('data', (data) => {
output += data.toString();
console.log('服务器输出:', data.toString());
});
server.stderr.on('data', (data) => {
console.error('服务器错误:', data.toString());
});
server.on('close', (code) => {
console.log(`服务器进程退出,代码: ${code}`);
});
// 等待服务器启动
setTimeout(() => {
console.log('\n检查服务器环境变量...');
console.log('DB_HOST:', process.env.DB_HOST);
console.log('DB_PORT:', process.env.DB_PORT);
console.log('DB_PASSWORD:', process.env.DB_PASSWORD);
// 测试API
const axios = require('axios');
axios.get('http://localhost:5350/api/smart-alerts/public/collar?search=22012000107&limit=1')
.then(response => {
console.log('\nAPI测试结果:');
if (response.data.success && response.data.data.length > 0) {
const collar = response.data.data[0];
console.log('项圈编号:', collar.collarNumber);
console.log('电量:', collar.battery);
console.log('温度:', collar.temperature);
}
})
.catch(error => {
console.error('API测试失败:', error.message);
})
.finally(() => {
server.kill();
process.exit(0);
});
}, 5000);

View File

@@ -1,118 +0,0 @@
/**
* 检查特定项圈数据
* @file check-specific-collar.js
* @description 检查项圈编号22012000107的数据
*/
const axios = require('axios');
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
async function checkSpecificCollar() {
console.log('🔍 检查项圈编号22012000107的数据...\n');
try {
// 1. 搜索特定项圈编号
console.log('1. 搜索项圈编号22012000107...');
const searchResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
search: '22012000107',
page: 1,
limit: 10
}
});
if (searchResponse.data.success) {
const data = searchResponse.data.data || [];
console.log(`找到 ${data.length} 条相关数据`);
data.forEach((item, index) => {
console.log(`\n${index + 1}条数据:`);
console.log('原始API数据:', {
id: item.id,
collarNumber: item.collarNumber,
battery: item.battery,
batteryLevel: item.batteryLevel,
temperature: item.temperature,
temp: item.temp,
alertType: item.alertType,
alertLevel: item.alertLevel,
alertTime: item.alertTime,
dailySteps: item.dailySteps,
steps: item.steps
});
// 模拟前端转换逻辑
const transformedData = {
id: item.id || `${item.deviceId || item.sn}_${item.alertType || 'normal'}`,
collarNumber: item.collarNumber || item.sn || item.deviceId || '',
battery: item.battery || item.batteryLevel || '',
temperature: item.temperature || item.temp || '',
dailySteps: item.dailySteps || item.steps || ''
};
console.log('前端转换后:', transformedData);
});
} else {
console.log('❌ 搜索失败:', searchResponse.data.message);
}
// 2. 获取所有数据并查找特定项圈
console.log('\n2. 获取所有数据查找特定项圈...');
const allResponse = await axios.get(`${BASE_URL}/collar`, {
params: { page: 1, limit: 100 }
});
if (allResponse.data.success) {
const allData = allResponse.data.data || [];
const specificCollars = allData.filter(item =>
item.collarNumber == 22012000107 ||
item.deviceName == 22012000107 ||
item.sn == 22012000107
);
console.log(`在所有数据中找到 ${specificCollars.length} 条项圈22012000107的数据`);
specificCollars.forEach((item, index) => {
console.log(`\n项圈22012000107 - 第${index + 1}条:`);
console.log('ID:', item.id);
console.log('项圈编号:', item.collarNumber);
console.log('设备名称:', item.deviceName);
console.log('电量字段1 (battery):', item.battery);
console.log('电量字段2 (batteryLevel):', item.batteryLevel);
console.log('温度字段1 (temperature):', item.temperature);
console.log('温度字段2 (temp):', item.temp);
console.log('预警类型:', item.alertType);
console.log('预警级别:', item.alertLevel);
console.log('预警时间:', item.alertTime);
console.log('步数字段1 (dailySteps):', item.dailySteps);
console.log('步数字段2 (steps):', item.steps);
console.log('总步数:', item.totalSteps);
console.log('昨日步数:', item.yesterdaySteps);
});
} else {
console.log('❌ 获取所有数据失败:', allResponse.data.message);
}
// 3. 检查数据库原始数据
console.log('\n3. 检查数据库原始数据...');
console.log('请检查数据库中项圈22012000107的原始数据');
console.log('可能需要检查以下字段:');
console.log('- battery_level 或 battery 字段');
console.log('- temperature 或 temp 字段');
console.log('- 数据更新时间');
console.log('- 是否有多个记录');
} catch (error) {
console.error('❌ 检查失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
}
}
// 运行检查
checkSpecificCollar().catch(console.error);

View File

@@ -1,61 +0,0 @@
@echo off
echo 正在配置Windows防火墙以允许外部访问...
echo.
REM 检查是否以管理员身份运行
net session >nul 2>&1
if %errorLevel% == 0 (
echo 检测到管理员权限,继续配置...
) else (
echo 错误:请以管理员身份运行此脚本
echo 右键点击此文件,选择"以管理员身份运行"
pause
exit /b 1
)
echo.
echo 添加防火墙规则...
REM 允许前端端口5300
netsh advfirewall firewall add rule name="Node.js Frontend Port 5300" dir=in action=allow protocol=TCP localport=5300
if %errorLevel% == 0 (
echo ✅ 前端端口5300规则添加成功
) else (
echo ⚠️ 前端端口5300规则可能已存在
)
REM 允许后端端口5350
netsh advfirewall firewall add rule name="Node.js Backend Port 5350" dir=in action=allow protocol=TCP localport=5350
if %errorLevel% == 0 (
echo ✅ 后端端口5350规则添加成功
) else (
echo ⚠️ 后端端口5350规则可能已存在
)
REM 允许Node.js程序
netsh advfirewall firewall add rule name="Node.js Program" dir=in action=allow program="C:\Program Files\nodejs\node.exe" enable=yes
if %errorLevel% == 0 (
echo ✅ Node.js程序规则添加成功
) else (
echo ⚠️ Node.js程序规则可能已存在
)
echo.
echo 检查已添加的规则...
netsh advfirewall firewall show rule name="Node.js Frontend Port 5300"
echo.
netsh advfirewall firewall show rule name="Node.js Backend Port 5350"
echo.
echo 🎉 防火墙配置完成!
echo.
echo 现在其他用户可以通过以下地址访问您的服务:
echo 前端: http://172.28.112.1:5300
echo 后端: http://172.28.112.1:5350
echo.
echo 请确保:
echo 1. 服务器正在运行
echo 2. 其他用户与您在同一个局域网内
echo 3. 使用正确的IP地址不是localhost
echo.
pause

View File

@@ -1,78 +0,0 @@
# PowerShell防火墙配置脚本
# 解决外部用户无法访问开发服务器的问题
Write-Host "🔧 正在配置Windows防火墙以允许外部访问..." -ForegroundColor Green
Write-Host ""
# 检查是否以管理员身份运行
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Host "❌ 错误:请以管理员身份运行此脚本" -ForegroundColor Red
Write-Host "右键点击此文件,选择'以管理员身份运行'" -ForegroundColor Yellow
Read-Host "按任意键退出"
exit 1
}
Write-Host "✅ 检测到管理员权限,继续配置..." -ForegroundColor Green
Write-Host ""
# 配置防火墙规则
Write-Host "添加防火墙规则..." -ForegroundColor Cyan
# 允许前端端口5300
try {
New-NetFirewallRule -DisplayName "Node.js Frontend Port 5300" -Direction Inbound -Protocol TCP -LocalPort 5300 -Action Allow -ErrorAction SilentlyContinue
Write-Host "✅ 前端端口5300规则添加成功" -ForegroundColor Green
} catch {
Write-Host "⚠️ 前端端口5300规则可能已存在" -ForegroundColor Yellow
}
# 允许后端端口5350
try {
New-NetFirewallRule -DisplayName "Node.js Backend Port 5350" -Direction Inbound -Protocol TCP -LocalPort 5350 -Action Allow -ErrorAction SilentlyContinue
Write-Host "✅ 后端端口5350规则添加成功" -ForegroundColor Green
} catch {
Write-Host "⚠️ 后端端口5350规则可能已存在" -ForegroundColor Yellow
}
# 允许Node.js程序
try {
$nodePath = "C:\Program Files\nodejs\node.exe"
if (Test-Path $nodePath) {
New-NetFirewallRule -DisplayName "Node.js Program" -Direction Inbound -Program $nodePath -Action Allow -ErrorAction SilentlyContinue
Write-Host "✅ Node.js程序规则添加成功" -ForegroundColor Green
} else {
Write-Host "⚠️ 未找到Node.js程序路径跳过程序规则" -ForegroundColor Yellow
}
} catch {
Write-Host "⚠️ Node.js程序规则可能已存在" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "检查已添加的规则..." -ForegroundColor Cyan
# 显示规则
Get-NetFirewallRule -DisplayName "*Node.js*" | Format-Table DisplayName, Direction, Action, Enabled -AutoSize
Write-Host ""
Write-Host "🎉 防火墙配置完成!" -ForegroundColor Green
Write-Host ""
Write-Host "现在其他用户可以通过以下地址访问您的服务:" -ForegroundColor Yellow
Write-Host "前端: http://172.28.112.1:5300" -ForegroundColor White
Write-Host "后端: http://172.28.112.1:5350" -ForegroundColor White
Write-Host ""
Write-Host "请确保:" -ForegroundColor Yellow
Write-Host "1. 服务器正在运行" -ForegroundColor White
Write-Host "2. 其他用户与您在同一个局域网内" -ForegroundColor White
Write-Host "3. 使用正确的IP地址不是localhost" -ForegroundColor White
Write-Host ""
# 获取所有可用的IP地址
Write-Host "可用的访问地址:" -ForegroundColor Cyan
$networkAdapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.254.*" }
foreach ($adapter in $networkAdapters) {
Write-Host " 前端: http://$($adapter.IPAddress):5300" -ForegroundColor White
Write-Host " 后端: http://$($adapter.IPAddress):5350" -ForegroundColor White
}
Write-Host ""
Read-Host "按任意键退出"

View File

@@ -113,6 +113,17 @@ class CattleExitRecordController {
async getExitRecordById(req, res) {
try {
const { id } = req.params;
console.log('=== 获取离栏记录详情 ===');
console.log('请求时间:', new Date().toISOString());
console.log('记录ID:', id);
console.log('请求来源:', req.ip);
console.log('用户信息:', req.user ? { id: req.user.id, username: req.user.username } : '未登录');
console.log('User-Agent:', req.get('User-Agent'));
console.log('请求URL:', req.originalUrl);
console.log('请求方法:', req.method);
console.log('Referer:', req.get('Referer'));
console.log('操作类型: 获取详情(可能用于编辑)');
const record = await CattleExitRecord.findByPk(id, {
attributes: ['id', 'recordId', 'animalId', 'earNumber', 'exitDate', 'exitReason', 'originalPenId', 'destination', 'disposalMethod', 'handler', 'status', 'remark', 'farmId', 'created_at', 'updated_at'],
@@ -136,19 +147,88 @@ class CattleExitRecordController {
});
if (!record) {
console.log('离栏记录不存在ID:', id);
return res.status(404).json({
success: false,
message: '离栏记录不存在'
});
}
console.log('找到离栏记录:', {
id: record.id,
recordId: record.recordId,
earNumber: record.earNumber,
exitDate: record.exitDate,
exitReason: record.exitReason,
originalPenId: record.originalPenId,
farmId: record.farmId,
status: record.status
});
// 格式化返回数据
const formattedData = {
id: record.id,
recordId: record.recordId,
animalId: record.animalId,
earNumber: record.earNumber,
exitDate: record.exitDate,
exitReason: record.exitReason,
originalPenId: record.originalPenId,
originalPen: record.originalPen ? {
id: record.originalPen.id,
name: record.originalPen.name,
code: record.originalPen.code
} : null,
destination: record.destination,
disposalMethod: record.disposalMethod,
handler: record.handler,
status: record.status,
remark: record.remark,
farmId: record.farmId,
farm: record.farm ? {
id: record.farm.id,
name: record.farm.name
} : null,
cattle: record.cattle ? {
id: record.cattle.id,
earNumber: record.cattle.earNumber,
strain: record.cattle.strain,
sex: record.cattle.sex
} : null,
created_at: record.created_at,
updated_at: record.updated_at
};
console.log('=== 返回格式化后的数据 ===');
console.log('格式化数据示例:', {
id: formattedData.id,
recordId: formattedData.recordId,
earNumber: formattedData.earNumber,
exitDate: formattedData.exitDate,
exitReason: formattedData.exitReason,
originalPenId: formattedData.originalPenId,
originalPenName: formattedData.originalPen?.name,
destination: formattedData.destination,
disposalMethod: formattedData.disposalMethod,
handler: formattedData.handler,
status: formattedData.status,
farmId: formattedData.farmId,
farmName: formattedData.farm?.name
});
console.log('完整返回数据字段:', Object.keys(formattedData));
res.json({
success: true,
data: record,
data: formattedData,
message: '获取离栏记录详情成功'
});
} catch (error) {
console.error('获取离栏记录详情失败:', error);
console.error('=== 获取离栏记录详情失败 ===');
console.error('错误时间:', new Date().toISOString());
console.error('记录ID:', req.params.id);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
res.status(500).json({
success: false,
message: '获取离栏记录详情失败',

View File

@@ -10,24 +10,38 @@ class CattlePenController {
*/
async getPens(req, res) {
try {
const { page = 1, pageSize = 10, search, status, type } = req.query;
const { page = 1, pageSize = 10, search, name, status, type } = req.query;
const offset = (page - 1) * pageSize;
console.log('=== 获取栏舍列表 ===');
console.log('请求时间:', new Date().toISOString());
console.log('请求参数:', { page, pageSize, search, name, status, type });
console.log('请求来源:', req.ip);
// 构建查询条件
const where = {};
if (search) {
// 支持 search 和 name 参数(兼容性处理)
const searchKeyword = search || name;
if (searchKeyword) {
console.log('🔍 [后端-栏舍设置] 搜索关键词:', searchKeyword);
where[Op.or] = [
{ name: { [Op.like]: `%${search}%` } },
{ code: { [Op.like]: `%${search}%` } }
{ name: { [Op.like]: `%${searchKeyword}%` } },
{ code: { [Op.like]: `%${searchKeyword}%` } }
];
console.log('🔍 [后端-栏舍设置] 搜索条件构建完成');
}
if (status) {
where.status = status;
}
if (type) {
where.type = type;
}
console.log('🔍 [后端-栏舍设置] 构建的查询条件:', JSON.stringify(where, null, 2));
console.log('🔍 [后端-栏舍设置] 开始执行查询...');
const { count, rows } = await CattlePen.findAndCountAll({
where,
include: [
@@ -42,6 +56,18 @@ class CattlePenController {
order: [['created_at', 'DESC']]
});
console.log('📊 [后端-栏舍设置] 查询结果:', {
总数: count,
当前页记录数: rows.length,
记录列表: rows.map(item => ({
id: item.id,
name: item.name,
code: item.code,
type: item.type,
status: item.status
}))
});
res.json({
success: true,
data: {

View File

@@ -1,4 +1,6 @@
const { Op } = require('sequelize');
const XLSX = require('xlsx');
const path = require('path');
const IotCattle = require('../models/IotCattle');
const Farm = require('../models/Farm');
const CattlePen = require('../models/CattlePen');
@@ -38,6 +40,77 @@ const getCategoryName = (cate) => {
return categoryMap[cate] || '未知';
};
/**
* 类别名称到ID的映射用于导入
*/
const getCategoryId = (name) => {
const categoryMap = {
'犊牛': 1,
'育成母牛': 2,
'架子牛': 3,
'青年牛': 4,
'基础母牛': 5,
'育肥牛': 6
};
return categoryMap[name] || null;
};
/**
* 性别名称到ID的映射用于导入
*/
const getSexId = (name) => {
const sexMap = {
'公': 1,
'公牛': 1,
'母': 2,
'母牛': 2
};
return sexMap[name] || null;
};
/**
* 来源名称到ID的映射用于导入
*/
const getSourceId = (name) => {
const sourceMap = {
'购买': 1,
'自繁': 2,
'放生': 3,
'合作社': 4,
'入股': 5
};
return sourceMap[name] || null;
};
/**
* 血统纯度名称到ID的映射用于导入
*/
const getDescentId = (name) => {
const descentMap = {
'纯血': 1,
'纯种': 1,
'杂交': 2,
'杂交一代': 2,
'杂交二代': 3,
'杂交三代': 4
};
return descentMap[name] || null;
};
/**
* 日期字符串转换为时间戳(秒)
*/
const dateToTimestamp = (dateStr) => {
if (!dateStr) return null;
// 支持多种日期格式2023-01-01, 2023/01/01, 2023-1-1
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
return null;
}
return Math.floor(date.getTime() / 1000);
};
/**
* 获取栏舍、批次、品种和用途名称
*/
@@ -224,9 +297,12 @@ class IotCattleController {
id: cattle.id,
earNumber: cattle.earNumber, // 映射iot_cattle.ear_number
sex: cattle.sex, // 映射iot_cattle.sex
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称(用于显示)
strainId: cattle.strain, // 原始ID用于编辑和提交
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称用于显示
varietiesId: cattle.varieties, // 原始ID用于编辑和提交
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文用于显示
cateId: cattle.cate, // 原始ID用于编辑和提交
birthWeight: cattle.birthWeight, // 映射iot_cattle.birth_weight
birthday: cattle.birthday, // 映射iot_cattle.birthday
intoTime: cattle.intoTime,
@@ -287,6 +363,13 @@ class IotCattleController {
async getCattleArchiveById(req, res) {
try {
const { id } = req.params;
console.log('=== 获取牛只档案详情 ===');
console.log('请求时间:', new Date().toISOString());
console.log('档案ID:', id);
console.log('请求来源:', req.ip);
console.log('用户信息:', req.user ? { id: req.user.id, username: req.user.username } : '未登录');
console.log('User-Agent:', req.get('User-Agent'));
const cattle = await IotCattle.findByPk(id, {
include: [
@@ -309,20 +392,36 @@ class IotCattleController {
});
if (!cattle) {
console.log('牛只档案不存在ID:', id);
return res.status(404).json({
success: false,
message: '牛只档案不存在'
});
}
console.log('找到牛只档案:', {
id: cattle.id,
earNumber: cattle.earNumber,
orgId: cattle.orgId,
penId: cattle.penId,
batchId: cattle.batchId
});
// 获取栏舍、批次、品种和用途名称
const cattleList = [cattle];
const { penNames, batchNames, typeNames, userNames } = await getPenBatchTypeAndUserNames(cattleList);
// 格式化数据基于iot_cattle表字段映射
const formattedData = {
id: cattle.id,
earNumber: cattle.earNumber, // 映射iot_cattle.ear_number
sex: cattle.sex, // 映射iot_cattle.sex
strain: cattle.strain, // 映射iot_cattle.strain
varieties: cattle.varieties, // 映射iot_cattle.varieties单个记录不需要名称映射
cate: cattle.cate, // 映射iot_cattle.cate
strain: userNames[cattle.strain] || `品系ID:${cattle.strain}`, // 映射iot_cattle.strain为用途名称(用于显示)
strainId: cattle.strain, // 原始ID用于编辑和提交
varieties: typeNames[cattle.varieties] || `品种ID:${cattle.varieties}`, // 映射iot_cattle.varieties为品种名称用于显示
varietiesId: cattle.varieties, // 原始ID用于编辑和提交
cate: getCategoryName(cattle.cate), // 映射iot_cattle.cate为中文用于显示
cateId: cattle.cate, // 原始ID用于编辑和提交
birthWeight: cattle.birthWeight, // 映射iot_cattle.birth_weight
birthday: cattle.birthday, // 映射iot_cattle.birthday
intoTime: cattle.intoTime,
@@ -336,20 +435,34 @@ class IotCattleController {
weightCalculateTime: cattle.weightCalculateTime,
dayOfBirthday: cattle.dayOfBirthday,
farmName: `农场ID:${cattle.orgId}`, // 暂时显示ID后续可优化
penName: cattle.penId ? `栏舍ID:${cattle.penId}` : '未分配栏舍', // 暂时显示ID后续可优化
batchName: cattle.batchId === 0 ? '未分配批次' : `批次ID:${cattle.batchId}`, // 暂时显示ID后续可优化
penName: cattle.penId ? (penNames[cattle.penId] || `栏舍ID:${cattle.penId}`) : '未分配栏舍', // 映射栏舍名称
batchName: cattle.batchId === 0 ? '未分配批次' : (batchNames[cattle.batchId] || `批次ID:${cattle.batchId}`), // 映射批次名称
farmId: cattle.orgId, // 映射iot_cattle.org_id
penId: cattle.penId, // 映射iot_cattle.pen_id
batchId: cattle.batchId // 映射iot_cattle.batch_id
};
console.log('=== 返回格式化后的数据 ===');
console.log('格式化数据示例:', {
id: formattedData.id,
earNumber: formattedData.earNumber,
farmId: formattedData.farmId,
penId: formattedData.penId,
batchId: formattedData.batchId
});
res.json({
success: true,
data: formattedData,
message: '获取牛只档案详情成功'
});
} catch (error) {
console.error('获取牛只档案详情失败:', error);
console.error('=== 获取牛只档案详情失败 ===');
console.error('错误时间:', new Date().toISOString());
console.error('档案ID:', req.params.id);
console.error('错误信息:', error.message);
console.error('错误堆栈:', error.stack);
res.status(500).json({
success: false,
message: '获取牛只档案详情失败',
@@ -500,23 +613,84 @@ class IotCattleController {
}
}
// 转换数据类型
// 转换数据类型,只更新实际提交的字段
const processedData = {};
if (updateData.earNumber) processedData.earNumber = parseInt(updateData.earNumber);
if (updateData.sex) processedData.sex = parseInt(updateData.sex);
if (updateData.strain) processedData.strain = parseInt(updateData.strain);
if (updateData.varieties) processedData.varieties = parseInt(updateData.varieties);
if (updateData.cate) processedData.cate = parseInt(updateData.cate);
if (updateData.birthWeight) processedData.birthWeight = parseFloat(updateData.birthWeight);
if (updateData.birthday) processedData.birthday = parseInt(updateData.birthday);
if (updateData.penId) processedData.penId = parseInt(updateData.penId);
if (updateData.intoTime) processedData.intoTime = parseInt(updateData.intoTime);
if (updateData.parity) processedData.parity = parseInt(updateData.parity);
if (updateData.source) processedData.source = parseInt(updateData.source);
if (updateData.sourceDay) processedData.sourceDay = parseInt(updateData.sourceDay);
if (updateData.sourceWeight) processedData.sourceWeight = parseFloat(updateData.sourceWeight);
if (updateData.orgId) processedData.orgId = parseInt(updateData.orgId);
if (updateData.batchId) processedData.batchId = parseInt(updateData.batchId);
// 辅助函数:安全转换为整数,如果转换失败则返回原值(不更新该字段)
const safeParseInt = (value) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = parseInt(value);
return isNaN(parsed) ? undefined : parsed;
};
// 辅助函数:安全转换为浮点数,如果转换失败则返回原值(不更新该字段)
const safeParseFloat = (value) => {
if (value === null || value === undefined || value === '') return undefined;
const parsed = parseFloat(value);
return isNaN(parsed) ? undefined : parsed;
};
// 只更新实际提交的字段,如果字段值无效则跳过(保持原有值)
if (updateData.hasOwnProperty('earNumber')) {
const parsed = safeParseInt(updateData.earNumber);
if (parsed !== undefined) processedData.earNumber = parsed;
}
if (updateData.hasOwnProperty('sex')) {
const parsed = safeParseInt(updateData.sex);
if (parsed !== undefined) processedData.sex = parsed;
}
if (updateData.hasOwnProperty('strain')) {
const parsed = safeParseInt(updateData.strain);
if (parsed !== undefined) processedData.strain = parsed;
}
if (updateData.hasOwnProperty('varieties')) {
const parsed = safeParseInt(updateData.varieties);
if (parsed !== undefined) processedData.varieties = parsed;
}
if (updateData.hasOwnProperty('cate')) {
const parsed = safeParseInt(updateData.cate);
if (parsed !== undefined) processedData.cate = parsed;
}
if (updateData.hasOwnProperty('birthWeight')) {
const parsed = safeParseFloat(updateData.birthWeight);
if (parsed !== undefined) processedData.birthWeight = parsed;
}
if (updateData.hasOwnProperty('birthday')) {
const parsed = safeParseInt(updateData.birthday);
if (parsed !== undefined) processedData.birthday = parsed;
}
if (updateData.hasOwnProperty('penId')) {
const parsed = safeParseInt(updateData.penId);
if (parsed !== undefined) processedData.penId = parsed;
}
if (updateData.hasOwnProperty('intoTime')) {
const parsed = safeParseInt(updateData.intoTime);
if (parsed !== undefined) processedData.intoTime = parsed;
}
if (updateData.hasOwnProperty('parity')) {
const parsed = safeParseInt(updateData.parity);
if (parsed !== undefined) processedData.parity = parsed;
}
if (updateData.hasOwnProperty('source')) {
const parsed = safeParseInt(updateData.source);
if (parsed !== undefined) processedData.source = parsed;
}
if (updateData.hasOwnProperty('sourceDay')) {
const parsed = safeParseInt(updateData.sourceDay);
if (parsed !== undefined) processedData.sourceDay = parsed;
}
if (updateData.hasOwnProperty('sourceWeight')) {
const parsed = safeParseFloat(updateData.sourceWeight);
if (parsed !== undefined) processedData.sourceWeight = parsed;
}
if (updateData.hasOwnProperty('orgId')) {
const parsed = safeParseInt(updateData.orgId);
if (parsed !== undefined) processedData.orgId = parsed;
}
if (updateData.hasOwnProperty('batchId')) {
const parsed = safeParseInt(updateData.batchId);
if (parsed !== undefined) processedData.batchId = parsed;
}
await cattle.update(processedData);
@@ -728,22 +902,280 @@ class IotCattleController {
});
}
// 这里需要添加Excel解析逻辑
// 由于没有安装xlsx库先返回模拟数据
const importedCount = 0;
const errors = [];
// 解析Excel文件
const workbook = XLSX.readFile(file.path);
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet);
// TODO: 实现Excel文件解析和数据库插入逻辑
// 1. 使用xlsx库解析Excel文件
// 2. 验证数据格式
// 3. 批量插入到数据库
// 4. 返回导入结果
console.log(`解析到 ${data.length} 行数据`);
if (data.length === 0) {
return res.status(400).json({
success: false,
message: 'Excel文件中没有数据'
});
}
// 获取所有品种和品系(品类)的映射关系
const cattleTypes = await CattleType.findAll({ attributes: ['id', 'name'] });
const typeNameToId = {};
cattleTypes.forEach(type => {
typeNameToId[type.name] = type.id;
});
const cattleUsers = await CattleUser.findAll({ attributes: ['id', 'name'] });
const userNameToId = {};
cattleUsers.forEach(user => {
userNameToId[user.name] = user.id;
});
// 获取所有栏舍和批次的映射关系
const pens = await CattlePen.findAll({ attributes: ['id', 'name'] });
const penNameToId = {};
pens.forEach(pen => {
penNameToId[pen.name] = pen.id;
});
const batches = await CattleBatch.findAll({ attributes: ['id', 'name'] });
const batchNameToId = {};
batches.forEach(batch => {
batchNameToId[batch.name] = batch.id;
});
// 获取默认农场ID从请求中获取或使用第一个农场
let defaultOrgId = req.body.orgId || req.query.orgId;
if (!defaultOrgId) {
const firstFarm = await Farm.findOne({ order: [['id', 'ASC']] });
defaultOrgId = firstFarm ? firstFarm.id : null;
}
if (!defaultOrgId) {
return res.status(400).json({
success: false,
message: '请指定所属农场'
});
}
const errors = [];
const successData = [];
// 处理每一行数据
for (let i = 0; i < data.length; i++) {
const row = data[i];
const rowNum = i + 2; // Excel行号从2开始第1行是表头
try {
// 验证必填字段
if (!row['耳号']) {
errors.push({ row: rowNum, field: '耳号', message: '耳号不能为空' });
continue;
}
// 映射字段
const earNumber = String(row['耳号']).trim();
// 检查耳号是否已存在
const existingCattle = await IotCattle.findOne({
where: { earNumber: parseInt(earNumber) }
});
if (existingCattle) {
errors.push({ row: rowNum, field: '耳号', message: `耳号 ${earNumber} 已存在` });
continue;
}
// 品类strain- 从名称查找ID必填
const strainName = row['品类'] ? String(row['品类']).trim() : '';
if (!strainName) {
errors.push({ row: rowNum, field: '品类', message: '品类不能为空' });
continue;
}
const strainId = userNameToId[strainName];
if (!strainId) {
errors.push({ row: rowNum, field: '品类', message: `品类 "${strainName}" 不存在` });
continue;
}
// 品种varieties- 从名称查找ID必填
const varietiesName = row['品种'] ? String(row['品种']).trim() : '';
if (!varietiesName) {
errors.push({ row: rowNum, field: '品种', message: '品种不能为空' });
continue;
}
const varietiesId = typeNameToId[varietiesName];
if (!varietiesId) {
errors.push({ row: rowNum, field: '品种', message: `品种 "${varietiesName}" 不存在` });
continue;
}
// 生理阶段cate必填
const cateName = row['生理阶段'] ? String(row['生理阶段']).trim() : '';
if (!cateName) {
errors.push({ row: rowNum, field: '生理阶段', message: '生理阶段不能为空' });
continue;
}
const cateId = getCategoryId(cateName);
if (!cateId) {
errors.push({ row: rowNum, field: '生理阶段', message: `生理阶段 "${cateName}" 无效` });
continue;
}
// 性别sex必填
const sexName = row['性别'] ? String(row['性别']).trim() : '';
if (!sexName) {
errors.push({ row: rowNum, field: '性别', message: '性别不能为空' });
continue;
}
const sexId = getSexId(sexName);
if (!sexId) {
errors.push({ row: rowNum, field: '性别', message: `性别 "${sexName}" 无效,应为"公"或"母"` });
continue;
}
// 来源source必填
const sourceName = row['来源'] ? String(row['来源']).trim() : '';
if (!sourceName) {
errors.push({ row: rowNum, field: '来源', message: '来源不能为空' });
continue;
}
const sourceId = getSourceId(sourceName);
if (!sourceId) {
errors.push({ row: rowNum, field: '来源', message: `来源 "${sourceName}" 无效` });
continue;
}
// 血统纯度descent
const descentName = row['血统纯度'] ? String(row['血统纯度']).trim() : '';
const descentId = descentName ? getDescentId(descentName) : 0;
// 栏舍penId- 从名称查找ID
const penName = row['栏舍'] ? String(row['栏舍']).trim() : '';
const penId = penName ? (penNameToId[penName] || null) : null;
// 所属批次batchId- 从名称查找ID
const batchName = row['所属批次'] ? String(row['所属批次']).trim() : '';
const batchId = batchName ? (batchNameToId[batchName] || null) : null;
// 已产胎次parity
const parity = row['已产胎次'] ? parseInt(row['已产胎次']) || 0 : 0;
// 出生日期birthday必填
const birthdayStr = row['出生日期(格式必须为2023-01-01)'] || row['出生日期'] || '';
if (!birthdayStr) {
errors.push({ row: rowNum, field: '出生日期', message: '出生日期不能为空' });
continue;
}
const birthday = dateToTimestamp(birthdayStr);
if (!birthday) {
errors.push({ row: rowNum, field: '出生日期', message: `出生日期格式错误: "${birthdayStr}"格式应为2023-01-01` });
continue;
}
// 现估重weight必填
const currentWeightStr = row['现估重(公斤)'] || row['现估重'] || '';
if (!currentWeightStr) {
errors.push({ row: rowNum, field: '现估重(公斤)', message: '现估重(公斤)不能为空' });
continue;
}
const currentWeight = parseFloat(currentWeightStr);
if (isNaN(currentWeight) || currentWeight < 0) {
errors.push({ row: rowNum, field: '现估重(公斤)', message: `现估重(公斤)格式错误: "${currentWeightStr}"` });
continue;
}
// 代数algebra
const algebra = row['代数'] ? parseInt(row['代数']) || 0 : 0;
// 入场日期intoTime
const intoTimeStr = row['入场日期(格式必须为2023-01-01)'] || row['入场日期'] || '';
const intoTime = intoTimeStr ? dateToTimestamp(intoTimeStr) : null;
if (intoTimeStr && !intoTime) {
errors.push({ row: rowNum, field: '入场日期', message: `入场日期格式错误: "${intoTimeStr}"` });
continue;
}
// 出生体重birthWeight
const birthWeight = row['出生体重'] ? parseFloat(row['出生体重']) || 0 : 0;
// 冻精编号semenNum
const semenNum = row['冻精编号'] ? String(row['冻精编号']).trim() : '';
// 构建插入数据
const cattleData = {
orgId: parseInt(defaultOrgId),
earNumber: parseInt(earNumber),
sex: sexId,
strain: strainId || 0,
varieties: varietiesId || 0,
cate: cateId || 0,
birthWeight: birthWeight,
birthday: birthday || 0,
penId: penId || 0,
intoTime: intoTime || 0,
parity: parity,
source: sourceId || 0,
sourceDay: 0,
sourceWeight: 0,
weight: currentWeight,
event: 1,
eventTime: Math.floor(Date.now() / 1000),
lactationDay: 0,
semenNum: semenNum,
isWear: 0,
imgs: '',
isEleAuth: 0,
isQuaAuth: 0,
isDelete: 0,
isOut: 0,
createUid: req.user ? req.user.id : 1,
createTime: Math.floor(Date.now() / 1000),
algebra: algebra,
colour: '',
infoWeight: 0,
descent: descentId || 0,
isVaccin: 0,
isInsemination: 0,
isInsure: 0,
isMortgage: 0,
updateTime: Math.floor(Date.now() / 1000),
breedBullTime: 0,
level: 0,
sixWeight: 0,
eighteenWeight: 0,
twelveDayWeight: 0,
eighteenDayWeight: 0,
xxivDayWeight: 0,
semenBreedImgs: '',
sellStatus: 100,
batchId: batchId || 0
};
// 插入数据库
await IotCattle.create(cattleData);
successData.push({ row: rowNum, earNumber: earNumber });
} catch (error) {
console.error(`处理第 ${rowNum} 行数据失败:`, error);
errors.push({
row: rowNum,
field: '数据',
message: `处理失败: ${error.message}`
});
}
}
const importedCount = successData.length;
console.log(`导入完成: 成功 ${importedCount} 条,失败 ${errors.length}`);
res.json({
success: true,
message: '导入功能开发中',
importedCount,
errors
message: `导入完成: 成功 ${importedCount} 条,失败 ${errors.length}`,
importedCount: importedCount,
errorCount: errors.length,
errors: errors,
successData: successData
});
} catch (error) {
@@ -763,55 +1195,65 @@ class IotCattleController {
try {
console.log('=== 下载牛只档案导入模板 ===');
// 创建模板数据 - 按照截图格式
// 创建模板数据 - 按照图片格式16列
const templateData = [
{
'耳标编号': '2105523006',
'性别': '1为公牛2为母牛',
'品': '1乳肉兼用',
'品种': '1:西藏高山牦牛2:宁夏牛',
'别': '1:犊牛,2:育成母牛,3:架子牛,4:青年牛,5:基础母牛,6:育肥牛',
'出生体重(kg)': '30',
'出生日期': '格式必须为2023-1-15',
'栏舍ID': '1',
'入栏时间': '2023-01-20',
'胎次': '0',
'来源': '1',
'来源天数': '5',
'来源体重': '35.5',
'当前体重': '450.0',
'事件': '正常',
'事件时间': '2023-01-20',
'泌乳天数': '0',
'精液编号': '',
'是否佩戴': '1',
'批次ID': '1'
'耳号': '202308301035',
'品类': '肉用型牛',
'品': '蒙古牛',
'生理阶段': '牛',
'别': '',
'血统纯度': '纯血',
'栏舍': '牛舍-20230819',
'所属批次': '230508357',
'已产胎次': '0',
'来源': '购买',
'现估重(公斤)': '50',
'数': '0',
'出生日期(格式必须为2023-01-01)': '2023-08-30',
'入场日期(格式必须为2023-01-01)': '2023-08-30',
'出生体重': '50.00',
'冻精编号': '51568'
},
{
'耳号': '202308301036',
'品类': '肉用型牛',
'品种': '蒙古牛',
'生理阶段': '犊牛',
'性别': '母',
'血统纯度': '杂交',
'栏舍': '牛舍-20230819',
'所属批次': '230508357',
'已产胎次': '1',
'来源': '购买',
'现估重(公斤)': '50',
'代数': '1',
'出生日期(格式必须为2023-01-01)': '2023-08-30',
'入场日期(格式必须为2023-01-01)': '2023-08-30',
'出生体重': '45.00',
'冻精编号': '51568'
}
];
// 使用ExportUtils生成Excel文件
// 使用ExportUtils生成Excel文件,按照图片中的列顺序
const ExportUtils = require('../utils/exportUtils');
const result = ExportUtils.exportToExcel(templateData, [
{ title: '耳标编号', dataIndex: '耳标编号', key: 'earNumber' },
{ title: '性别', dataIndex: '性别', key: 'sex' },
{ title: '品', dataIndex: '品', key: 'strain' },
{ title: '品种', dataIndex: '品种', key: 'varieties' },
{ title: '别', dataIndex: '别', key: 'cate' },
{ title: '出生体重(kg)', dataIndex: '出生体重(kg)', key: 'birthWeight' },
{ title: '出生日期', dataIndex: '出生日期', key: 'birthday' },
{ title: '栏舍ID', dataIndex: '栏舍ID', key: 'penId' },
{ title: '入栏时间', dataIndex: '入栏时间', key: 'intoTime' },
{ title: '胎次', dataIndex: '胎次', key: 'parity' },
{ title: '来源', dataIndex: '来源', key: 'source' },
{ title: '来源天数', dataIndex: '来源天数', key: 'sourceDay' },
{ title: '来源体重', dataIndex: '来源体重', key: 'sourceWeight' },
{ title: '当前体重', dataIndex: '当前体重', key: 'weight' },
{ title: '事件', dataIndex: '事件', key: 'event' },
{ title: '事件时间', dataIndex: '事件时间', key: 'eventTime' },
{ title: '泌乳天数', dataIndex: '泌乳天数', key: 'lactationDay' },
{ title: '精液编号', dataIndex: '精液编号', key: 'semenNum' },
{ title: '是否佩戴', dataIndex: '是否佩戴', key: 'isWear' },
{ title: '批次ID', dataIndex: '批次ID', key: 'batchId' }
const result = await ExportUtils.exportToExcelWithStyle(templateData, [
{ title: '耳号', dataIndex: '耳号', key: 'earNumber', width: 15, required: true },
{ title: '品类', dataIndex: '品类', key: 'strain', width: 12, required: true },
{ title: '品', dataIndex: '品', key: 'varieties', width: 12, required: true },
{ title: '生理阶段', dataIndex: '生理阶段', key: 'cate', width: 12, required: true },
{ title: '别', dataIndex: '别', key: 'sex', width: 8, required: true },
{ title: '血统纯度', dataIndex: '血统纯度', key: 'descent', width: 12, required: false },
{ title: '栏舍', dataIndex: '栏舍', key: 'penName', width: 15, required: false },
{ title: '所属批次', dataIndex: '所属批次', key: 'batchName', width: 15, required: false },
{ title: '已产胎次', dataIndex: '已产胎次', key: 'parity', width: 10, required: false },
{ title: '来源', dataIndex: '来源', key: 'source', width: 10, required: true },
{ title: '现估重(公斤)', dataIndex: '现估重(公斤)', key: 'currentWeight', width: 12, required: true },
{ title: '数', dataIndex: '数', key: 'algebra', width: 8, required: false },
{ title: '出生日期(格式必须为2023-01-01)', dataIndex: '出生日期(格式必须为2023-01-01)', key: 'birthday', width: 25, required: true },
{ title: '入场日期(格式必须为2023-01-01)', dataIndex: '入场日期(格式必须为2023-01-01)', key: 'intoTime', width: 25, required: false },
{ title: '出生体重', dataIndex: '出生体重', key: 'birthWeight', width: 12, required: false },
{ title: '冻精编号', dataIndex: '冻精编号', key: 'semenNum', width: 12, required: false }
], '牛只档案导入模板');
if (result.success) {

View File

@@ -1,35 +0,0 @@
console.log('开始调试启动过程...');
try {
console.log('1. 加载数据库配置...');
const { sequelize } = require('./config/database-simple');
console.log('✓ 数据库配置加载成功');
console.log('2. 加载模型...');
const models = require('./models');
console.log('✓ 模型加载成功');
console.log('3. 测试数据库连接...');
sequelize.authenticate().then(() => {
console.log('✓ 数据库连接成功');
console.log('4. 测试用户查询...');
return models.User.findByPk(1);
}).then(user => {
if (user) {
console.log('✓ 用户查询成功:', user.username);
} else {
console.log('⚠ 未找到用户');
}
console.log('调试完成,所有测试通过');
process.exit(0);
}).catch(error => {
console.error('❌ 错误:', error.message);
console.error('堆栈:', error.stack);
process.exit(1);
});
} catch (error) {
console.error('❌ 启动失败:', error.message);
console.error('堆栈:', error.stack);
process.exit(1);
}

View File

@@ -1,43 +0,0 @@
@echo off
echo ========================================
echo ngrok外网访问演示脚本
echo ========================================
echo.
echo 步骤1检查ngrok是否已配置认证
.\ngrok.exe config check
echo.
echo 步骤2如果没有配置请先运行以下命令
echo .\ngrok.exe authtoken YOUR_AUTH_TOKEN
echo.
echo 步骤3启动后端服务穿透
echo 正在启动ngrok...
echo 请在新窗口中查看访问地址
echo.
start "ngrok-backend" .\ngrok.exe http 5350
echo.
echo 步骤4等待3秒后启动前端服务穿透
timeout /t 3 /nobreak >nul
start "ngrok-frontend" .\ngrok.exe http 5300
echo.
echo ========================================
echo ngrok已启动
echo ========================================
echo.
echo 请查看两个新打开的窗口:
echo 1. ngrok-backend 窗口:显示后端访问地址
echo 2. ngrok-frontend 窗口:显示前端访问地址
echo.
echo 访问地址格式:
echo - 后端https://xxxxx.ngrok.io
echo - 前端https://yyyyy.ngrok.io
echo - API文档https://xxxxx.ngrok.io/api-docs
echo.
echo 按任意键退出...
pause >nul

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# 修复行尾符问题的脚本
echo "修复 node_manager.sh 的行尾符..."
# 方法1: 使用 sed (推荐)
sed -i 's/\r$//' node_manager.sh
# 设置执行权限
chmod +x node_manager.sh
echo "修复完成!"
echo "现在可以运行: ./node_manager.sh start"

View File

@@ -263,6 +263,10 @@ const generateOperationDesc = (req, responseBody) => {
if (url.includes('/reports')) {
return `查看${moduleName}报表`;
}
// 如果是获取单个记录详情,可能是用于编辑
if (url.match(/\/\d+$/)) {
return `获取${moduleName}详情`;
}
return `查看${moduleName}数据`;
default:

289
backend/node_manager.ps1 Normal file
View File

@@ -0,0 +1,289 @@
# Node.js 服务管理脚本 - 宁夏智慧养殖监管平台后端 (Windows版本)
# 使用方法: .\node_manager.ps1 [start|stop|restart|status]
param(
[Parameter(Mandatory=$false)]
[ValidateSet('start','stop','restart','status','logs')]
[string]$Action = 'status'
)
# 配置区域
$APP_NAME = "nxxmdata-backend"
$ENTRY_FILE = "server.js"
$APP_PORT = 5350
$LOG_DIR = "logs"
$LOG_FILE = Join-Path $LOG_DIR "$APP_NAME.log"
$PID_FILE = "pid.$APP_NAME"
$NODE_ENV = "production"
# 创建日志目录
if (-not (Test-Path $LOG_DIR)) {
New-Item -ItemType Directory -Path $LOG_DIR | Out-Null
Write-Host "已创建日志目录: $LOG_DIR" -ForegroundColor Green
}
# 检查 Node.js 是否安装
try {
$nodeVersion = node --version
Write-Host "Node.js 版本: $nodeVersion" -ForegroundColor Green
} catch {
Write-Host "错误: Node.js 未安装或不在 PATH 中" -ForegroundColor Red
exit 1
}
# 检查入口文件是否存在
if (-not (Test-Path $ENTRY_FILE)) {
Write-Host "错误: 入口文件 $ENTRY_FILE 不存在" -ForegroundColor Red
exit 1
}
# 停止服务函数
function Stop-App {
Write-Host "正在停止服务: $APP_NAME" -ForegroundColor Yellow
# 查找占用端口的进程
$processes = Get-NetTCPConnection -LocalPort $APP_PORT -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty OwningProcess -Unique
if ($processes) {
foreach ($pid in $processes) {
try {
$process = Get-Process -Id $pid -ErrorAction SilentlyContinue
if ($process -and $process.ProcessName -eq "node") {
Write-Host "找到进程 PID: $pid,正在停止..." -ForegroundColor Yellow
Stop-Process -Id $pid -Force
Write-Host "服务已停止 (PID: $pid)" -ForegroundColor Green
}
} catch {
Write-Host "停止进程失败: $_" -ForegroundColor Red
}
}
# 清理 PID 文件
if (Test-Path $PID_FILE) {
Remove-Item $PID_FILE -Force
}
} else {
Write-Host "未找到运行中的服务 (端口 $APP_PORT 未被占用)" -ForegroundColor Yellow
}
}
# 启动服务函数
function Start-App {
Write-Host "正在启动服务: $APP_NAME" -ForegroundColor Yellow
# 检查端口是否被占用
$portCheck = Get-NetTCPConnection -LocalPort $APP_PORT -ErrorAction SilentlyContinue
if ($portCheck) {
$pid = $portCheck | Select-Object -First 1 -ExpandProperty OwningProcess
Write-Host "错误: 端口 $APP_PORT 已被占用 (PID: $pid)" -ForegroundColor Red
Write-Host "请先停止占用端口的进程或使用 restart 命令" -ForegroundColor Yellow
return
}
# 检查 .env 文件是否存在
if (-not (Test-Path ".env")) {
Write-Host "警告: .env 文件不存在,将使用默认配置" -ForegroundColor Yellow
Write-Host "建议从 env.example 复制并配置 .env 文件" -ForegroundColor Yellow
}
# 检查 node_modules 是否存在
if (-not (Test-Path "node_modules")) {
Write-Host "错误: node_modules 目录不存在" -ForegroundColor Red
Write-Host "请先运行: npm install" -ForegroundColor Yellow
return
}
# 启动 Node.js 应用
Write-Host "启动命令: node $ENTRY_FILE" -ForegroundColor Cyan
# 设置环境变量并启动
$env:NODE_ENV = $NODE_ENV
# 使用 Start-Process 启动后台进程
$processInfo = Start-Process -FilePath "node" `
-ArgumentList $ENTRY_FILE `
-RedirectStandardOutput $LOG_FILE `
-RedirectStandardError $LOG_FILE `
-WindowStyle Hidden `
-PassThru
if ($processInfo) {
$PID = $processInfo.Id
$PID | Out-File -FilePath $PID_FILE -Encoding ASCII
Write-Host "等待服务启动..." -ForegroundColor Yellow
Start-Sleep -Seconds 3
# 验证启动是否成功
if (Get-Process -Id $PID -ErrorAction SilentlyContinue) {
Write-Host "========================================" -ForegroundColor Green
Write-Host "服务启动成功!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host "应用名称: $APP_NAME"
Write-Host "进程 ID: $PID"
Write-Host "监听端口: $APP_PORT"
Write-Host "日志文件: $LOG_FILE"
Write-Host "PID 文件: $PID_FILE"
Write-Host "环境变量: NODE_ENV=$NODE_ENV"
Write-Host "API 文档: http://localhost:${APP_PORT}/api-docs" -ForegroundColor Cyan
# 检查端口监听状态
Start-Sleep -Seconds 2
$portCheck = Get-NetTCPConnection -LocalPort $APP_PORT -ErrorAction SilentlyContinue
if ($portCheck) {
Write-Host "✓ 端口 $APP_PORT 正在监听" -ForegroundColor Green
} else {
Write-Host "⚠ 端口 $APP_PORT 尚未监听,请检查日志" -ForegroundColor Yellow
}
# 显示最近的日志
Write-Host ""
Write-Host "最近的启动日志:" -ForegroundColor Yellow
Write-Host "------------------------"
if (Test-Path $LOG_FILE) {
Get-Content $LOG_FILE -Tail 20 -ErrorAction SilentlyContinue
}
} else {
Write-Host "========================================" -ForegroundColor Red
Write-Host "服务启动失败!" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Red
Write-Host "请检查日志文件: $LOG_FILE" -ForegroundColor Yellow
if (Test-Path $LOG_FILE) {
Write-Host ""
Write-Host "最近的错误日志:" -ForegroundColor Yellow
Write-Host "------------------------"
Get-Content $LOG_FILE -Tail 30 -ErrorAction SilentlyContinue
}
if (Test-Path $PID_FILE) {
Remove-Item $PID_FILE -Force
}
exit 1
}
} else {
Write-Host "启动进程失败!" -ForegroundColor Red
exit 1
}
}
# 重启服务函数
function Restart-App {
Write-Host "正在重启服务: $APP_NAME" -ForegroundColor Yellow
Stop-App
Start-Sleep -Seconds 2
Start-App
}
# 状态检查函数
function Get-AppStatus {
Write-Host "检查服务状态: $APP_NAME" -ForegroundColor Yellow
Write-Host "========================================"
# 检查端口占用
$portCheck = Get-NetTCPConnection -LocalPort $APP_PORT -ErrorAction SilentlyContinue
if ($portCheck) {
$pid = $portCheck | Select-Object -First 1 -ExpandProperty OwningProcess
$process = Get-Process -Id $pid -ErrorAction SilentlyContinue
if ($process -and $process.ProcessName -eq "node") {
Write-Host "✓ 服务正在运行" -ForegroundColor Green
Write-Host "----------------------------------------"
Write-Host "进程 ID: $pid"
Write-Host "进程名称: $($process.ProcessName)"
Write-Host "应用名称: $APP_NAME"
Write-Host "监听端口: $APP_PORT"
Write-Host "启动时间: $($process.StartTime)"
# 内存使用 (转换为 MB)
$memoryMB = [math]::Round($process.WorkingSet64 / 1MB, 2)
Write-Host "内存使用: $memoryMB MB"
# CPU时间
Write-Host "CPU 时间: $($process.CPU)"
# 端口状态
Write-Host "端口状态: 监听中 ($APP_PORT)" -ForegroundColor Green
# 日志文件信息
if (Test-Path $LOG_FILE) {
$logSize = [math]::Round((Get-Item $LOG_FILE).Length / 1KB, 2)
Write-Host "日志大小: $logSize KB"
Write-Host "日志文件: $LOG_FILE"
}
Write-Host ""
Write-Host "最近的日志 (最后10行):" -ForegroundColor Yellow
Write-Host "----------------------------------------"
if (Test-Path $LOG_FILE) {
Get-Content $LOG_FILE -Tail 10 -ErrorAction SilentlyContinue
} else {
Write-Host "无法读取日志文件"
}
} else {
Write-Host "⚠ 端口 $APP_PORT 被其他进程占用 (PID: $pid)" -ForegroundColor Yellow
Write-Host "进程名称: $($process.ProcessName)"
}
} else {
Write-Host "✗ 服务未运行" -ForegroundColor Red
Write-Host "使用以下命令启动: .\node_manager.ps1 start"
}
Write-Host "========================================"
}
# 查看日志函数
function View-Logs {
if (Test-Path $LOG_FILE) {
Write-Host "实时查看日志 (Ctrl+C 退出):" -ForegroundColor Yellow
Get-Content $LOG_FILE -Wait -Tail 20
} else {
Write-Host "日志文件不存在: $LOG_FILE" -ForegroundColor Red
}
}
# 主逻辑
switch ($Action.ToLower()) {
'start' {
Start-App
}
'stop' {
Stop-App
}
'restart' {
Restart-App
}
'status' {
Get-AppStatus
}
'logs' {
View-Logs
}
default {
Write-Host "========================================"
Write-Host "宁夏智慧养殖监管平台 - 服务管理脚本 (Windows)"
Write-Host "========================================"
Write-Host "使用方法: .\node_manager.ps1 [start|stop|restart|status|logs]"
Write-Host ""
Write-Host "命令说明:"
Write-Host " start - 启动服务"
Write-Host " stop - 停止服务"
Write-Host " restart - 重启服务"
Write-Host " status - 查看服务状态"
Write-Host " logs - 实时查看日志"
Write-Host ""
Write-Host "配置信息:"
Write-Host " 应用名称: $APP_NAME"
Write-Host " 入口文件: $ENTRY_FILE"
Write-Host " 监听端口: $APP_PORT"
Write-Host " 日志文件: $LOG_FILE"
Write-Host ""
Write-Host "示例:"
Write-Host " .\node_manager.ps1 start # 启动服务"
Write-Host " .\node_manager.ps1 status # 查看状态"
Write-Host " .\node_manager.ps1 logs # 查看日志"
}
}

328
backend/node_manager.sh Normal file
View File

@@ -0,0 +1,328 @@
#!/bin/bash
# Node.js 服务管理脚本 - 宁夏智慧养殖监管平台后端
# 使用方法: ./node_manager.sh [start|stop|restart|status]
# 配置区域
APP_NAME="nxxmdata-backend"
ENTRY_FILE="server.js"
APP_PORT="5350"
LOG_DIR="logs"
LOG_FILE="${LOG_DIR}/${APP_NAME}.log"
PID_FILE="pid.${APP_NAME}"
NODE_ENV="production"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 创建日志目录
if [ ! -d "$LOG_DIR" ]; then
mkdir -p "$LOG_DIR"
echo "已创建日志目录: $LOG_DIR"
fi
# 检查 Node.js 是否安装
if ! command -v node &> /dev/null; then
echo -e "${RED}错误: Node.js 未安装或不在 PATH 中${NC}"
exit 1
fi
# 检查入口文件是否存在
if [ ! -f "$ENTRY_FILE" ]; then
echo -e "${RED}错误: 入口文件 $ENTRY_FILE 不存在${NC}"
exit 1
fi
# 显示 Node.js 版本信息
NODE_VERSION=$(node --version)
echo -e "${GREEN}Node.js 版本: $NODE_VERSION${NC}"
# 停止服务函数
function stopApp() {
echo -e "${YELLOW}正在停止服务: $APP_NAME${NC}"
# 从 PID 文件读取进程 ID
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
echo "找到进程 PID: $PID,正在停止..."
kill -TERM $PID
# 等待进程优雅退出
for i in {1..10}; do
if ! ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}服务已优雅停止${NC}"
rm -f "$PID_FILE"
return 0
fi
sleep 1
done
# 如果优雅停止失败,强制杀死
echo -e "${YELLOW}优雅停止失败,强制终止进程${NC}"
kill -9 $PID
rm -f "$PID_FILE"
else
echo "PID 文件存在但进程不存在,清理 PID 文件"
rm -f "$PID_FILE"
fi
else
# 如果没有 PID 文件,尝试通过端口查找
PID=$(lsof -ti:$APP_PORT 2>/dev/null)
if [ -n "$PID" ]; then
echo "通过端口 $APP_PORT 找到 PID: $PID,正在停止..."
kill -TERM $PID
sleep 2
if ps -p $PID > /dev/null 2>&1; then
kill -9 $PID
fi
echo -e "${GREEN}服务已停止${NC}"
else
echo -e "${YELLOW}未找到运行中的服务: $APP_NAME${NC}"
fi
fi
}
# 启动服务函数
function startApp() {
echo -e "${YELLOW}正在启动服务: $APP_NAME${NC}"
# 检查服务是否已经运行
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
echo -e "${YELLOW}服务已在运行中 (PID: $PID)${NC}"
return 0
else
echo "PID 文件存在但进程不存在,清理后重新启动"
rm -f "$PID_FILE"
fi
fi
# 检查端口是否被占用
if command -v lsof &> /dev/null; then
PORT_CHECK=$(lsof -ti:$APP_PORT 2>/dev/null)
if [ -n "$PORT_CHECK" ]; then
echo -e "${RED}错误: 端口 $APP_PORT 已被占用 (PID: $PORT_CHECK)${NC}"
echo "请先停止占用端口的进程或使用 restart 命令"
return 1
fi
fi
# 检查 .env 文件是否存在
if [ ! -f ".env" ]; then
echo -e "${YELLOW}警告: .env 文件不存在,将使用默认配置${NC}"
echo "建议从 env.example 复制并配置 .env 文件"
fi
# 检查 node_modules 是否存在
if [ ! -d "node_modules" ]; then
echo -e "${RED}错误: node_modules 目录不存在${NC}"
echo "请先运行: npm install"
return 1
fi
# 启动 Node.js 应用
echo "启动命令: NODE_ENV=$NODE_ENV nohup node $ENTRY_FILE > $LOG_FILE 2>&1 &"
NODE_ENV=$NODE_ENV nohup node $ENTRY_FILE > $LOG_FILE 2>&1 &
# 获取新进程的 PID
PID=$!
echo $PID > "$PID_FILE"
# 等待应用启动
echo "等待服务启动..."
sleep 3
# 验证启动是否成功
if ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}服务启动成功!${NC}"
echo -e "${GREEN}========================================${NC}"
echo "应用名称: $APP_NAME"
echo "进程 ID: $PID"
echo "监听端口: $APP_PORT"
echo "日志文件: $LOG_FILE"
echo "PID 文件: $PID_FILE"
echo "环境变量: NODE_ENV=$NODE_ENV"
echo -e "${GREEN}API 文档: http://localhost:$APP_PORT/api-docs${NC}"
echo ""
# 等待端口监听
echo "检查端口监听状态..."
sleep 2
if command -v lsof &> /dev/null; then
if lsof -ti:$APP_PORT > /dev/null 2>&1; then
echo -e "${GREEN}✓ 端口 $APP_PORT 正在监听${NC}"
else
echo -e "${YELLOW}⚠ 端口 $APP_PORT 尚未监听,请检查日志${NC}"
fi
fi
# 显示最近的日志
echo ""
echo "最近的启动日志:"
echo "------------------------"
tail -20 "$LOG_FILE"
else
echo -e "${RED}========================================${NC}"
echo -e "${RED}服务启动失败!${NC}"
echo -e "${RED}========================================${NC}"
echo "请检查日志文件: $LOG_FILE"
echo ""
echo "最近的错误日志:"
echo "------------------------"
tail -30 "$LOG_FILE"
rm -f "$PID_FILE"
exit 1
fi
}
# 重启服务函数
function restartApp() {
echo -e "${YELLOW}正在重启服务: $APP_NAME${NC}"
stopApp
sleep 3
startApp
}
# 状态检查函数
function statusApp() {
echo -e "${YELLOW}检查服务状态: $APP_NAME${NC}"
echo "========================================"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p $PID > /dev/null 2>&1; then
echo -e "${GREEN}✓ 服务正在运行${NC}"
echo "----------------------------------------"
echo "进程 ID: $PID"
echo "应用名称: $APP_NAME"
echo "监听端口: $APP_PORT"
# 启动时间
if command -v ps &> /dev/null; then
START_TIME=$(ps -o lstart= -p $PID 2>/dev/null)
if [ -n "$START_TIME" ]; then
echo "启动时间: $START_TIME"
fi
# 内存使用
MEM_USAGE=$(ps -o rss= -p $PID 2>/dev/null | awk '{printf "%.2f MB", $1/1024}')
if [ -n "$MEM_USAGE" ]; then
echo "内存使用: $MEM_USAGE"
fi
# CPU使用率
CPU_USAGE=$(ps -o %cpu= -p $PID 2>/dev/null)
if [ -n "$CPU_USAGE" ]; then
echo "CPU 使用: ${CPU_USAGE}%"
fi
fi
# 检查端口监听状态
if command -v lsof &> /dev/null; then
PORT_INFO=$(lsof -ti:$APP_PORT 2>/dev/null)
if [ -n "$PORT_INFO" ]; then
echo -e "端口状态: ${GREEN}监听中 ($APP_PORT)${NC}"
else
echo -e "端口状态: ${RED}未监听${NC}"
fi
fi
# 日志文件信息
if [ -f "$LOG_FILE" ]; then
LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
echo "日志大小: $LOG_SIZE"
echo "日志文件: $LOG_FILE"
fi
echo ""
echo "最近的日志 (最后10行):"
echo "----------------------------------------"
tail -10 "$LOG_FILE" 2>/dev/null || echo "无法读取日志文件"
else
echo -e "${RED}✗ 服务未运行 (PID 文件存在但进程不存在)${NC}"
echo "建议清理 PID 文件: rm -f $PID_FILE"
fi
else
# 通过端口检查
if command -v lsof &> /dev/null; then
PID=$(lsof -ti:$APP_PORT 2>/dev/null)
if [ -n "$PID" ]; then
echo -e "${YELLOW}⚠ 服务正在运行但未通过脚本管理${NC}"
echo "进程 ID: $PID"
echo "监听端口: $APP_PORT"
echo "建议使用: ./node_manager.sh stop 停止服务后重新启动"
else
echo -e "${RED}✗ 服务未运行${NC}"
echo "使用以下命令启动: ./node_manager.sh start"
fi
else
echo -e "${RED}✗ 服务未运行 (PID 文件不存在)${NC}"
echo "使用以下命令启动: ./node_manager.sh start"
fi
fi
echo "========================================"
}
# 查看日志函数
function viewLogs() {
if [ -f "$LOG_FILE" ]; then
echo -e "${YELLOW}实时查看日志 (Ctrl+C 退出):${NC}"
tail -f "$LOG_FILE"
else
echo -e "${RED}日志文件不存在: $LOG_FILE${NC}"
fi
}
# 主逻辑
case "$1" in
start)
startApp
;;
stop)
stopApp
;;
restart)
restartApp
;;
status)
statusApp
;;
logs)
viewLogs
;;
*)
echo "========================================"
echo "宁夏智慧养殖监管平台 - 服务管理脚本"
echo "========================================"
echo "使用方法: $0 {start|stop|restart|status|logs}"
echo ""
echo "命令说明:"
echo " start - 启动服务"
echo " stop - 停止服务"
echo " restart - 重启服务"
echo " status - 查看服务状态"
echo " logs - 实时查看日志"
echo ""
echo "配置信息:"
echo " 应用名称: $APP_NAME"
echo " 入口文件: $ENTRY_FILE"
echo " 监听端口: $APP_PORT"
echo " 日志文件: $LOG_FILE"
echo ""
echo "示例:"
echo " $0 start # 启动服务"
echo " $0 status # 查看状态"
echo " $0 logs # 查看日志"
exit 1
;;
esac
exit 0

1293
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"exceljs": "^4.4.0",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-validator": "^7.0.1",
@@ -65,16 +66,16 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"nodemon": "^3.0.2",
"@types/jest": "^29.5.8",
"eslint": "^8.55.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"nodemon": "^3.0.2",
"rimraf": "^5.0.5",
"@types/jest": "^29.5.8"
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
@@ -88,10 +89,16 @@
"!**/seeds/**"
],
"coverageDirectory": "coverage",
"coverageReporters": ["text", "lcov", "html"]
"coverageReporters": [
"text",
"lcov",
"html"
]
},
"eslintConfig": {
"extends": ["standard"],
"extends": [
"standard"
],
"env": {
"node": true,
"es2021": true,

6631
backend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
/**
* 简单数据库测试
* @file simple-db-test.js
* @description 测试数据库连接和查询
*/
const { IotXqClient } = require('./models');
async function testDatabase() {
console.log('🔍 测试数据库连接...\n');
try {
// 测试数据库连接
console.log('1. 测试数据库连接...');
await IotXqClient.sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 查询项圈22012000107的数据
console.log('\n2. 查询项圈22012000107的数据...');
const devices = await IotXqClient.findAll({
where: {
sn: '22012000107'
},
order: [['uptime', 'DESC']],
limit: 5
});
console.log(`找到 ${devices.length} 条记录`);
devices.forEach((device, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', device.id);
console.log('SN:', device.sn);
console.log('电量:', device.battery);
console.log('温度:', device.temperature);
console.log('状态:', device.state);
console.log('更新时间:', device.uptime);
});
// 查询所有项圈数据
console.log('\n3. 查询所有项圈数据...');
const allDevices = await IotXqClient.findAll({
order: [['uptime', 'DESC']],
limit: 10
});
console.log(`总共 ${allDevices.length} 条记录`);
allDevices.forEach((device, index) => {
console.log(`\n设备${index + 1}:`);
console.log('SN:', device.sn);
console.log('电量:', device.battery);
console.log('温度:', device.temperature);
console.log('状态:', device.state);
});
} catch (error) {
console.error('❌ 测试失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行测试
testDatabase().catch(console.error);

View File

@@ -1,15 +0,0 @@
# 简化的防火墙配置脚本
Write-Host "正在配置Windows防火墙..." -ForegroundColor Green
# 添加前端端口规则
netsh advfirewall firewall add rule name="Node.js Frontend Port 5300" dir=in action=allow protocol=TCP localport=5300
Write-Host "前端端口5300规则已添加" -ForegroundColor Green
# 添加后端端口规则
netsh advfirewall firewall add rule name="Node.js Backend Port 5350" dir=in action=allow protocol=TCP localport=5350
Write-Host "后端端口5350规则已添加" -ForegroundColor Green
Write-Host "防火墙配置完成!" -ForegroundColor Green
Write-Host "现在其他用户可以通过以下地址访问:" -ForegroundColor Yellow
Write-Host "前端: http://172.28.112.1:5300" -ForegroundColor White
Write-Host "后端: http://172.28.112.1:5350" -ForegroundColor White

View File

@@ -1,72 +0,0 @@
// 简化的测试脚本
console.log('开始测试预警检测逻辑...');
// 模拟前端判断函数
function determineAlertType(record) {
const alerts = []
// 检查电量预警
if (record.battery !== undefined && record.battery !== null && record.battery < 20) {
alerts.push('battery')
}
// 检查脱落预警 (bandge_status为0)
if (record.bandge_status !== undefined && record.bandge_status !== null && record.bandge_status === 0) {
alerts.push('wear')
}
// 检查离线预警 (is_connect为0)
if (record.is_connect !== undefined && record.is_connect !== null && record.is_connect === 0) {
alerts.push('offline')
}
// 检查温度预警
if (record.temperature !== undefined && record.temperature !== null) {
if (record.temperature < 20) {
alerts.push('temperature_low')
} else if (record.temperature > 40) {
alerts.push('temperature_high')
}
}
// 检查运动异常预警 (steps - y_steps为0)
if (record.steps !== undefined && record.y_steps !== undefined &&
record.steps !== null && record.y_steps !== null) {
const movementDiff = record.steps - record.y_steps
if (movementDiff === 0) {
alerts.push('movement')
}
}
// 返回第一个预警类型如果没有预警则返回null
return alerts.length > 0 ? alerts[0] : null
}
// 测试用例
const testCases = [
{
name: '正常设备',
data: { battery: 85, temperature: 25, is_connect: 1, bandge_status: 1, steps: 1000, y_steps: 500 },
expected: null
},
{
name: '低电量预警',
data: { battery: 15, temperature: 25, is_connect: 1, bandge_status: 1, steps: 1000, y_steps: 500 },
expected: 'battery'
},
{
name: '离线预警',
data: { battery: 85, temperature: 25, is_connect: 0, bandge_status: 1, steps: 1000, y_steps: 500 },
expected: 'offline'
}
];
// 运行测试
testCases.forEach((testCase, index) => {
const result = determineAlertType(testCase.data);
const success = result === testCase.expected;
console.log(`测试 ${index + 1}: ${testCase.name} - ${success ? '✅ 通过' : '❌ 失败'}`);
console.log(` 预期: ${testCase.expected}, 实际: ${result}`);
});
console.log('测试完成!');

View File

@@ -1,146 +0,0 @@
/**
* 启动服务器并测试API
* @file start-and-test.js
* @description 启动服务器并自动测试API接口
*/
const { spawn } = require('child_process');
const axios = require('axios');
let serverProcess = null;
// 启动服务器
function startServer() {
return new Promise((resolve, reject) => {
console.log('🚀 启动服务器...');
serverProcess = spawn('node', ['server.js'], {
stdio: 'pipe',
cwd: __dirname
});
serverProcess.stdout.on('data', (data) => {
const output = data.toString();
console.log(output);
if (output.includes('服务器运行在端口') || output.includes('Server running on port')) {
console.log('✅ 服务器启动成功');
resolve();
}
});
serverProcess.stderr.on('data', (data) => {
console.error('服务器错误:', data.toString());
});
serverProcess.on('error', (error) => {
console.error('启动服务器失败:', error);
reject(error);
});
// 等待5秒让服务器完全启动
setTimeout(() => {
resolve();
}, 5000);
});
}
// 测试API
async function testAPI() {
console.log('\n🔍 测试API接口...');
const baseUrl = 'http://localhost:5350';
try {
// 1. 测试根路径
console.log('1. 测试根路径...');
const rootResponse = await axios.get(`${baseUrl}/`);
console.log('✅ 根路径正常:', rootResponse.data.message);
// 2. 测试API文档
console.log('\n2. 测试API文档...');
const docsResponse = await axios.get(`${baseUrl}/api-docs/`);
console.log('✅ API文档页面正常');
// 3. 测试Swagger JSON
console.log('\n3. 测试Swagger JSON...');
const swaggerResponse = await axios.get(`${baseUrl}/api-docs/swagger.json`);
console.log('✅ Swagger JSON正常');
// 4. 检查API路径
console.log('\n4. 检查API路径...');
const paths = Object.keys(swaggerResponse.data.paths || {});
const smartAlertPaths = paths.filter(path => path.includes('/smart-alerts/public'));
console.log(`📋 找到 ${smartAlertPaths.length} 个智能预警API路径:`);
smartAlertPaths.forEach(path => {
console.log(` - ${path}`);
});
if (smartAlertPaths.length > 0) {
console.log('\n🎉 API文档配置成功');
console.log(`📖 请访问: ${baseUrl}/api-docs`);
} else {
console.log('\n❌ 未找到智能预警API路径');
}
// 5. 测试实际API调用
console.log('\n5. 测试实际API调用...');
try {
const eartagStatsResponse = await axios.get(`${baseUrl}/api/smart-alerts/public/eartag/stats`);
console.log('✅ 智能耳标预警统计API正常');
} catch (error) {
console.log('⚠️ 智能耳标预警统计API调用失败:', error.message);
}
try {
const collarStatsResponse = await axios.get(`${baseUrl}/api/smart-alerts/public/collar/stats`);
console.log('✅ 智能项圈预警统计API正常');
} catch (error) {
console.log('⚠️ 智能项圈预警统计API调用失败:', error.message);
}
} catch (error) {
console.error('❌ API测试失败:', error.message);
}
}
// 停止服务器
function stopServer() {
if (serverProcess) {
console.log('\n🛑 停止服务器...');
serverProcess.kill();
serverProcess = null;
}
}
// 主函数
async function main() {
try {
await startServer();
await testAPI();
console.log('\n✅ 测试完成!');
console.log('📖 API文档地址: http://localhost:5350/api-docs');
console.log('🔗 基础API地址: http://localhost:5350/api/smart-alerts/public');
console.log('\n按 Ctrl+C 停止服务器');
// 保持服务器运行
process.on('SIGINT', () => {
stopServer();
process.exit(0);
});
} catch (error) {
console.error('❌ 启动失败:', error.message);
stopServer();
process.exit(1);
}
}
// 如果直接运行此脚本
if (require.main === module) {
main().catch(console.error);
}
module.exports = { startServer, testAPI, stopServer };

View File

@@ -1,46 +0,0 @@
@echo off
echo 正在启动ngrok外网访问服务...
echo.
REM 检查ngrok是否存在
if not exist "ngrok.exe" (
echo 错误ngrok.exe 不存在请先下载ngrok
pause
exit /b 1
)
echo 请确保您已经:
echo 1. 注册了ngrok账号
echo 2. 获取了authtoken
echo 3. 运行了ngrok authtoken YOUR_AUTH_TOKEN
echo.
echo 选择要启动的服务:
echo 1. 前端服务 (端口5300)
echo 2. 后端服务 (端口5350)
echo 3. 同时启动前端和后端
echo.
set /p choice="请输入选择 (1-3): "
if "%choice%"=="1" (
echo 启动前端服务穿透...
.\ngrok.exe http 5300
) else if "%choice%"=="2" (
echo 启动后端服务穿透...
.\ngrok.exe http 5350
) else if "%choice%"=="3" (
echo 启动前端服务穿透...
start "Frontend" .\ngrok.exe http 5300
timeout /t 3 /nobreak >nul
echo 启动后端服务穿透...
start "Backend" .\ngrok.exe http 5350
echo.
echo 两个服务都已启动,请查看各自的窗口获取访问地址
) else (
echo 无效选择,请重新运行脚本
)
echo.
echo 按任意键退出...
pause >nul

View File

@@ -1,68 +0,0 @@
# ngrok外网访问启动脚本
Write-Host "🚀 正在启动ngrok外网访问服务..." -ForegroundColor Green
Write-Host ""
# 检查ngrok是否存在
if (-not (Test-Path "ngrok.exe")) {
Write-Host "❌ 错误ngrok.exe 不存在请先下载ngrok" -ForegroundColor Red
Read-Host "按任意键退出"
exit 1
}
Write-Host "请确保您已经:" -ForegroundColor Yellow
Write-Host "1. 注册了ngrok账号" -ForegroundColor White
Write-Host "2. 获取了authtoken" -ForegroundColor White
Write-Host "3. 运行了ngrok authtoken YOUR_AUTH_TOKEN" -ForegroundColor White
Write-Host ""
Write-Host "选择要启动的服务:" -ForegroundColor Cyan
Write-Host "1. 前端服务 (端口5300)" -ForegroundColor White
Write-Host "2. 后端服务 (端口5350)" -ForegroundColor White
Write-Host "3. 同时启动前端和后端" -ForegroundColor White
Write-Host "4. 检查当前服务状态" -ForegroundColor White
Write-Host ""
$choice = Read-Host "请输入选择 (1-4)"
switch ($choice) {
"1" {
Write-Host "启动前端服务穿透..." -ForegroundColor Green
Write-Host "访问地址将在新窗口中显示" -ForegroundColor Yellow
Start-Process -FilePath ".\ngrok.exe" -ArgumentList "http", "5300"
}
"2" {
Write-Host "启动后端服务穿透..." -ForegroundColor Green
Write-Host "访问地址将在新窗口中显示" -ForegroundColor Yellow
Start-Process -FilePath ".\ngrok.exe" -ArgumentList "http", "5350"
}
"3" {
Write-Host "启动前端服务穿透..." -ForegroundColor Green
Start-Process -FilePath ".\ngrok.exe" -ArgumentList "http", "5300"
Start-Sleep -Seconds 3
Write-Host "启动后端服务穿透..." -ForegroundColor Green
Start-Process -FilePath ".\ngrok.exe" -ArgumentList "http", "5350"
Write-Host ""
Write-Host "✅ 两个服务都已启动,请查看各自的窗口获取访问地址" -ForegroundColor Green
}
"4" {
Write-Host "检查当前服务状态..." -ForegroundColor Cyan
Write-Host "前端服务 (端口5300):" -ForegroundColor Yellow
netstat -ano | findstr :5300
Write-Host ""
Write-Host "后端服务 (端口5350):" -ForegroundColor Yellow
netstat -ano | findstr :5350
}
default {
Write-Host "❌ 无效选择,请重新运行脚本" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "💡 提示:" -ForegroundColor Cyan
Write-Host "- 前端访问地址格式https://xxxxx.ngrok.io" -ForegroundColor White
Write-Host "- 后端访问地址格式https://yyyyy.ngrok.io" -ForegroundColor White
Write-Host "- API文档地址https://yyyyy.ngrok.io/api-docs" -ForegroundColor White
Write-Host ""
Read-Host "按任意键退出"

View File

@@ -1,45 +0,0 @@
@echo off
echo ========================================
echo 无密码外网访问启动脚本
echo ========================================
echo.
echo 选择要启动的服务:
echo 1. 后端服务 (端口5350)
echo 2. 前端服务 (端口5300)
echo 3. 同时启动两个服务
echo.
set /p choice="请输入选择 (1-3): "
if "%choice%"=="1" (
echo 启动后端服务穿透...
start "Backend Tunnel" lt --port 5350
) else if "%choice%"=="2" (
echo 启动前端服务穿透...
start "Frontend Tunnel" lt --port 5300
) else if "%choice%"=="3" (
echo 启动后端服务穿透...
start "Backend Tunnel" lt --port 5350
timeout /t 2 /nobreak >nul
echo 启动前端服务穿透...
start "Frontend Tunnel" lt --port 5300
) else (
echo 无效选择
goto end
)
echo.
echo ========================================
echo 隧道已启动!
echo ========================================
echo.
echo 请查看新打开的窗口获取访问地址
echo 地址格式: https://随机字符串.loca.lt
echo.
echo 这些地址没有密码保护,可以直接访问
echo.
:end
echo 按任意键退出...
pause >nul

21
backend/start.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
REM 宁夏智慧养殖监管平台后端 - 快速启动脚本
REM 使用 PowerShell 脚本启动服务
echo ========================================
echo 宁夏智慧养殖监管平台后端服务
echo ========================================
echo.
REM 检查是否在正确的目录
if not exist "server.js" (
echo 错误: 请在 backend 目录下运行此脚本
pause
exit /b 1
)
REM 调用 PowerShell 脚本启动服务
powershell -ExecutionPolicy Bypass -File "node_manager.ps1" start
pause

21
backend/status.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
REM 宁夏智慧养殖监管平台后端 - 状态查看脚本
REM 使用 PowerShell 脚本查看服务状态
echo ========================================
echo 宁夏智慧养殖监管平台后端服务
echo ========================================
echo.
REM 检查是否在正确的目录
if not exist "server.js" (
echo 错误: 请在 backend 目录下运行此脚本
pause
exit /b 1
)
REM 调用 PowerShell 脚本查看状态
powershell -ExecutionPolicy Bypass -File "node_manager.ps1" status
pause

21
backend/stop.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
REM 宁夏智慧养殖监管平台后端 - 快速停止脚本
REM 使用 PowerShell 脚本停止服务
echo ========================================
echo 宁夏智慧养殖监管平台后端服务
echo ========================================
echo.
REM 检查是否在正确的目录
if not exist "server.js" (
echo 错误: 请在 backend 目录下运行此脚本
pause
exit /b 1
)
REM 调用 PowerShell 脚本停止服务
powershell -ExecutionPolicy Bypass -File "node_manager.ps1" stop
pause

View File

@@ -1,773 +1,117 @@
/**
* 预警管理模块 Swagger 文档
* @file swagger-alerts.js
* @description 预警管理相关的 Swagger API 文档定义
*/
// 预警管理相关的 API 路径定义
const alertsPaths = {
// 获取所有预警
'/alerts': {
'/api/alerts': {
get: {
summary: '获取所有预警',
tags: ['预警管理'],
summary: '获取预警列表',
description: '分页获取系统中的所有预警信息',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词(预警标题、描述)'
},
{
name: 'type',
in: 'query',
schema: { type: 'string', enum: ['health', 'environment', 'device', 'security', 'breeding'] },
description: '预警类型筛选'
},
{
name: 'level',
in: 'query',
schema: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
description: '预警级别筛选'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['pending', 'processing', 'resolved', 'ignored'] },
description: '预警状态筛选'
},
{
name: 'farmId',
in: 'query',
schema: { type: 'integer' },
description: '养殖场ID筛选'
}
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Alert' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
}
}
},
'401': {
description: '未授权',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
},
post: {
tags: ['预警管理'],
summary: '创建新预警',
description: '创建新的预警记录',
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['title', 'type', 'level', 'farmId'],
properties: {
title: { type: 'string', description: '预警标题' },
description: { type: 'string', description: '预警描述' },
type: {
type: 'string',
enum: ['health', 'environment', 'device', 'security', 'breeding'],
description: '预警类型health-健康environment-环境device-设备security-安全breeding-繁殖'
},
level: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: '预警级别low-低medium-中high-高critical-紧急'
},
farmId: { type: 'integer', description: '养殖场ID' },
animalId: { type: 'integer', description: '动物ID可选' },
deviceId: { type: 'integer', description: '设备ID可选' },
threshold: { type: 'number', description: '阈值' },
currentValue: { type: 'number', description: '当前值' },
location: { type: 'string', description: '位置信息' },
metadata: { type: 'object', description: '额外元数据' }
}
}
}
}
},
responses: {
'201': {
description: '创建成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '预警创建成功' },
data: { $ref: '#/components/schemas/Alert' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
// 获取公共预警数据
'/alerts/public': {
'/api/alerts/{id}': {
get: {
summary: '根据ID获取预警',
tags: ['预警管理'],
summary: '获取公共预警数据',
description: '获取可公开访问的预警基本信息',
security: [], // 公共接口不需要认证
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
title: { type: 'string' },
type: { type: 'string' },
level: { type: 'string' },
status: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}
}
}
}
},
// 搜索预警
'/alerts/search': {
get: {
tags: ['预警管理'],
summary: '搜索预警',
description: '根据养殖场名称等关键词搜索预警',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'q',
in: 'query',
required: true,
schema: { type: 'string' },
description: '搜索关键词'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '返回结果数量限制'
}
],
responses: {
'200': {
description: '搜索成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Alert' }
}
}
}
}
}
}
}
}
},
// 获取预警详情
'/alerts/{id}': {
get: {
tags: ['预警管理'],
summary: '获取预警详情',
description: '根据预警ID获取详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '预警ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: { $ref: '#/components/schemas/Alert' }
}
}
}
}
},
'404': {
description: '预警不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
put: {
tags: ['预警管理'],
summary: '更新预警信息',
description: '更新指定预警的信息',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '预警ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
title: { type: 'string', description: '预警标题' },
description: { type: 'string', description: '预警描述' },
type: {
type: 'string',
enum: ['health', 'environment', 'device', 'security', 'breeding'],
description: '预警类型'
},
level: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: '预警级别'
},
status: {
type: 'string',
enum: ['pending', 'processing', 'resolved', 'ignored'],
description: '预警状态'
},
threshold: { type: 'number', description: '阈值' },
currentValue: { type: 'number', description: '当前值' },
location: { type: 'string', description: '位置信息' },
handlerNotes: { type: 'string', description: '处理备注' },
metadata: { type: 'object', description: '额外元数据' }
}
}
}
}
},
responses: {
'200': {
description: '更新成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '预警信息更新成功' },
data: { $ref: '#/components/schemas/Alert' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '预警不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
},
delete: {
tags: ['预警管理'],
summary: '删除预警',
description: '删除指定预警(软删除)',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '预警ID'
}
],
responses: {
'200': {
description: '删除成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '预警删除成功' }
}
}
}
}
},
'404': {
description: '预警不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
}
},
// 更新预警状态
'/alerts/{id}/status': {
put: {
tags: ['预警管理'],
summary: '更新预警状态',
description: '更新指定预警的处理状态',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '预警ID'
}
],
tags: ['预警管理'],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['status'],
properties: {
status: {
type: 'string',
enum: ['pending', 'processing', 'resolved', 'ignored'],
description: '预警状态'
},
handlerNotes: { type: 'string', description: '处理备注' },
handlerId: { type: 'integer', description: '处理人ID' }
}
}
}
}
}
},
responses: {
'200': {
description: '状态更新成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '预警状态更新成功' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '预警不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
}
},
// 获取预警统计信息
'/alerts/stats/type': {
'/api/alerts/stats/type': {
get: {
summary: '获取预警类型统计',
tags: ['预警管理'],
summary: '获取按类型统计的预警数据',
description: '获取各种预警类型的统计信息',
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'object',
properties: {
health: { type: 'integer', description: '健康预警数量' },
environment: { type: 'integer', description: '环境预警数量' },
device: { type: 'integer', description: '设备预警数量' },
security: { type: 'integer', description: '安全预警数量' },
breeding: { type: 'integer', description: '繁殖预警数量' }
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
'/alerts/stats/level': {
'/api/alerts/stats/level': {
get: {
summary: '获取预警级别统计',
tags: ['预警管理'],
summary: '获取按级别统计的预警数据',
description: '获取各种预警级别的统计信息',
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'object',
properties: {
low: { type: 'integer', description: '低级预警数量' },
medium: { type: 'integer', description: '中级预警数量' },
high: { type: 'integer', description: '高级预警数量' },
critical: { type: 'integer', description: '紧急预警数量' }
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
'/alerts/stats/status': {
'/api/alerts/stats/status': {
get: {
summary: '获取预警状态统计',
tags: ['预警管理'],
summary: '获取按状态统计的预警数据',
description: '获取各种预警状态的统计信息',
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'object',
properties: {
pending: { type: 'integer', description: '待处理预警数量' },
processing: { type: 'integer', description: '处理中预警数量' },
resolved: { type: 'integer', description: '已解决预警数量' },
ignored: { type: 'integer', description: '已忽略预警数量' }
}
}
}
}
}
}
}
}
}
},
// 批量操作预警
'/alerts/batch': {
post: {
tags: ['预警管理'],
summary: '批量操作预警',
description: '批量更新预警状态或删除预警',
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['ids', 'action'],
properties: {
ids: {
type: 'array',
items: { type: 'integer' },
description: '预警ID列表'
},
action: {
type: 'string',
enum: ['resolve', 'ignore', 'delete', 'reopen'],
description: '操作类型resolve-解决ignore-忽略delete-删除reopen-重新打开'
},
handlerNotes: { type: 'string', description: '处理备注' }
}
}
}
}
},
responses: {
'200': {
description: '批量操作成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '批量操作完成' },
data: {
type: 'object',
properties: {
successCount: { type: 'integer', description: '成功处理数量' },
failedCount: { type: 'integer', description: '失败数量' },
failedIds: {
type: 'array',
items: { type: 'integer' },
description: '失败的预警ID列表'
}
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
}
};
// 数据模型定义
// 预警管理相关的数据模型定义
const alertSchemas = {
Alert: {
type: 'object',
properties: {
id: { type: 'integer', description: '预警ID' },
title: { type: 'string', description: '预警标题' },
description: { type: 'string', description: '预警描述' },
type: {
type: 'string',
enum: ['health', 'environment', 'device', 'security', 'breeding'],
description: '预警类型health-健康environment-环境device-设备security-安全breeding-繁殖'
},
level: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: '预警级别low-低medium-中high-高critical-紧急'
},
status: {
type: 'string',
enum: ['pending', 'processing', 'resolved', 'ignored'],
description: '预警状态pending-待处理processing-处理中resolved-已解决ignored-已忽略'
},
farmId: { type: 'integer', description: '养殖场ID' },
farmName: { type: 'string', description: '养殖场名称' },
animalId: { type: 'integer', description: '动物ID可选' },
animalEarNumber: { type: 'string', description: '动物耳标号(可选)' },
deviceId: { type: 'integer', description: '设备ID可选' },
deviceName: { type: 'string', description: '设备名称(可选)' },
threshold: { type: 'number', description: '阈值' },
currentValue: { type: 'number', description: '当前值' },
location: { type: 'string', description: '位置信息' },
handlerId: { type: 'integer', description: '处理人ID' },
handlerName: { type: 'string', description: '处理人姓名' },
handlerNotes: { type: 'string', description: '处理备注' },
handledAt: { type: 'string', format: 'date-time', description: '处理时间' },
metadata: { type: 'object', description: '额外元数据' },
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
}
},
AlertInput: {
type: 'object',
required: ['title', 'type', 'level', 'farmId'],
properties: {
title: { type: 'string', description: '预警标题' },
description: { type: 'string', description: '预警描述' },
type: {
type: 'string',
enum: ['health', 'environment', 'device', 'security', 'breeding'],
description: '预警类型'
},
level: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: '预警级别'
},
farmId: { type: 'integer', description: '养殖场ID' },
animalId: { type: 'integer', description: '动物ID可选' },
deviceId: { type: 'integer', description: '设备ID可选' },
threshold: { type: 'number', description: '阈值' },
currentValue: { type: 'number', description: '当前值' },
location: { type: 'string', description: '位置信息' },
metadata: { type: 'object', description: '额外元数据' }
}
},
AlertUpdate: {
type: 'object',
properties: {
title: { type: 'string', description: '预警标题' },
description: { type: 'string', description: '预警描述' },
type: {
type: 'string',
enum: ['health', 'environment', 'device', 'security', 'breeding'],
description: '预警类型'
},
level: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical'],
description: '预警级别'
},
type: { type: 'string', description: '预警类型' },
level: { type: 'string', enum: ['high', 'medium', 'low'], description: '预警级别' },
status: {
type: 'string',
enum: ['pending', 'processing', 'resolved', 'ignored'],
description: '预警状态'
},
threshold: { type: 'number', description: '阈值' },
currentValue: { type: 'number', description: '当前值' },
location: { type: 'string', description: '位置信息' },
handlerNotes: { type: 'string', description: '处理备注' },
metadata: { type: 'object', description: '额外元数据' }
}
},
AlertStats: {
type: 'object',
properties: {
totalAlerts: { type: 'integer', description: '总预警数' },
pendingAlerts: { type: 'integer', description: '待处理预警数' },
resolvedAlerts: { type: 'integer', description: '已解决预警数' },
criticalAlerts: { type: 'integer', description: '紧急预警数' },
todayAlerts: { type: 'integer', description: '今日新增预警数' },
byType: {
type: 'object',
properties: {
health: { type: 'integer' },
environment: { type: 'integer' },
device: { type: 'integer' },
security: { type: 'integer' },
breeding: { type: 'integer' }
}
},
byLevel: {
type: 'object',
properties: {
low: { type: 'integer' },
medium: { type: 'integer' },
high: { type: 'integer' },
critical: { type: 'integer' }
}
},
byStatus: {
type: 'object',
properties: {
pending: { type: 'integer' },
processing: { type: 'integer' },
resolved: { type: 'integer' },
ignored: { type: 'integer' }
}
}
title: { type: 'string', description: '预警标题' },
message: { type: 'string', description: '预警消息' },
deviceId: { type: 'integer', description: '设备ID' },
animalId: { type: 'integer', description: '动物ID' },
farmId: { type: 'integer', description: '养殖场ID' },
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
}
}
};
module.exports = { alertsPaths, alertSchemas };
module.exports = {
alertsPaths,
alertSchemas
};

View File

@@ -1,706 +1,90 @@
/**
* 动物管理模块 Swagger 文档
* @file swagger-animals.js
* @description 动物管理相关的 Swagger API 文档定义
*/
// 动物管理相关的 API 路径定义
const animalsPaths = {
// 获取所有动物列表
'/animals': {
'/api/animals': {
get: {
summary: '获取所有动物',
tags: ['动物管理'],
summary: '获取动物列表',
description: '分页获取系统中的所有动物信息',
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词(项圈编号、耳标号)'
},
{
name: 'farmId',
in: 'query',
schema: { type: 'integer' },
description: '养殖场ID筛选'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['healthy', 'sick', 'quarantine', 'sold'] },
description: '动物状态筛选'
},
{
name: 'category',
in: 'query',
schema: { type: 'integer', enum: [1, 2, 3, 4, 5, 6] },
description: '动物类别筛选1-犊牛2-育成母牛3-架子牛4-青年牛5-基础母牛6-育肥牛'
},
{
name: 'sex',
in: 'query',
schema: { type: 'integer', enum: [1, 2] },
description: '性别筛选1-公牛2-母牛'
}
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Animal' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
}
}
}
}
},
post: {
tags: ['动物管理'],
summary: '创建新动物档案',
description: '创建新的动物档案记录',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['earNumber', 'sex', 'varieties', 'cate', 'farmId'],
properties: {
earNumber: { type: 'string', description: '耳标号' },
sex: { type: 'integer', enum: [1, 2], description: '性别1-公牛2-母牛' },
strain: { type: 'string', description: '品系' },
varieties: { type: 'integer', description: '品种ID' },
cate: {
type: 'integer',
enum: [1, 2, 3, 4, 5, 6],
description: '类别1-犊牛2-育成母牛3-架子牛4-青年牛5-基础母牛6-育肥牛'
},
birthWeight: { type: 'number', description: '出生重量kg' },
birthday: { type: 'integer', description: '出生日期(时间戳)' },
farmId: { type: 'integer', description: '养殖场ID' },
penId: { type: 'integer', description: '栏舍ID' },
batchId: { type: 'integer', description: '批次ID' },
intoTime: { type: 'integer', description: '入场时间(时间戳)' },
parity: { type: 'integer', description: '胎次' },
source: {
type: 'integer',
enum: [1, 2, 3, 4, 5],
description: '来源1-合作社2-农户3-养殖场4-进口5-自繁'
},
sourceDay: { type: 'integer', description: '来源日龄' },
sourceWeight: { type: 'number', description: '来源重量kg' },
weight: { type: 'number', description: '当前重量kg' },
algebra: { type: 'string', description: '代数' },
colour: { type: 'string', description: '毛色' },
descent: { type: 'string', description: '血统' },
imgs: { type: 'string', description: '图片URL多个用逗号分隔' }
}
}
}
}
},
responses: {
'201': {
description: '创建成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '动物档案创建成功' },
data: { $ref: '#/components/schemas/Animal' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
// 公共动物数据
'/animals/public': {
'/api/animals/public': {
get: {
summary: '获取所有动物(公开接口)',
tags: ['动物管理'],
summary: '获取公共动物数据',
description: '获取可公开访问的动物基本信息',
security: [], // 公共接口不需要认证
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
earNumber: { type: 'string' },
sex: { type: 'integer' },
varieties: { type: 'integer' },
cate: { type: 'integer' },
weight: { type: 'number' },
isOut: { type: 'integer' }
}
}
},
message: { type: 'string' }
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
// 获取动物绑定信息
'/animals/binding-info/{collarNumber}': {
'/api/animals/binding-info/{collarNumber}': {
get: {
tags: ['动物管理'],
summary: '获取动物绑定信息',
description: '根据项圈编号获取动物的详细绑定信息',
tags: ['动物管理'],
parameters: [
{
name: 'collarNumber',
in: 'path',
required: true,
{
name: 'collarNumber',
in: 'path',
required: true,
schema: { type: 'string' },
description: '项圈编号'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string' },
data: {
type: 'object',
properties: {
basicInfo: {
type: 'object',
properties: {
collarNumber: { type: 'string', description: '项圈编号' },
category: { type: 'string', description: '动物类别' },
calvingCount: { type: 'integer', description: '产犊次数' },
earTag: { type: 'string', description: '耳标号' },
animalType: { type: 'string', description: '动物类型' },
breed: { type: 'string', description: '品种' },
sourceType: { type: 'string', description: '来源类型' }
}
},
birthInfo: {
type: 'object',
properties: {
birthDate: { type: 'string', description: '出生日期' },
birthWeight: { type: 'string', description: '出生重量' },
weaningWeight: { type: 'string', description: '断奶重量' },
entryDate: { type: 'string', description: '入场日期' },
weaningAge: { type: 'integer', description: '断奶日龄' }
}
},
pedigreeInfo: {
type: 'object',
properties: {
fatherId: { type: 'string', description: '父亲ID' },
motherId: { type: 'string', description: '母亲ID' },
grandfatherId: { type: 'string', description: '祖父ID' },
grandmotherId: { type: 'string', description: '祖母ID' },
bloodline: { type: 'string', description: '血统' },
generation: { type: 'string', description: '世代' }
}
},
insuranceInfo: {
type: 'object',
properties: {
policyNumber: { type: 'string', description: '保单号' },
insuranceCompany: { type: 'string', description: '保险公司' },
coverageAmount: { type: 'string', description: '保额' },
premium: { type: 'string', description: '保费' },
startDate: { type: 'string', description: '开始日期' },
endDate: { type: 'string', description: '结束日期' },
status: { type: 'string', description: '保险状态' }
}
},
loanInfo: {
type: 'object',
properties: {
loanNumber: { type: 'string', description: '贷款编号' },
bankName: { type: 'string', description: '银行名称' },
loanAmount: { type: 'string', description: '贷款金额' },
interestRate: { type: 'string', description: '利率' },
loanDate: { type: 'string', description: '放款日期' },
maturityDate: { type: 'string', description: '到期日期' },
status: { type: 'string', description: '贷款状态' }
}
},
deviceInfo: {
type: 'object',
properties: {
deviceId: { type: 'integer', description: '设备ID' },
batteryLevel: { type: 'number', description: '电池电量' },
temperature: { type: 'number', description: '温度' },
status: { type: 'string', description: '设备状态' },
lastUpdate: { type: 'string', description: '最后更新时间' },
location: { type: 'string', description: '位置信息' }
}
},
farmInfo: {
type: 'object',
properties: {
farmName: { type: 'string', description: '农场名称' },
farmAddress: { type: 'string', description: '农场地址' },
penName: { type: 'string', description: '栏舍名称' },
batchName: { type: 'string', description: '批次名称' }
}
}
}
}
}
}
}
}
},
'404': {
description: '未找到指定的动物或设备',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
}
},
// 获取指定动物详情
'/animals/{id}': {
get: {
tags: ['动物管理'],
summary: '获取动物详情',
description: '根据动物ID获取详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '动物ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: { $ref: '#/components/schemas/Animal' }
}
}
}
}
},
'404': {
description: '动物不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
},
put: {
tags: ['动物管理'],
summary: '更新动物信息',
description: '更新指定动物的档案信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '动物ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
earNumber: { type: 'string', description: '耳标号' },
sex: { type: 'integer', enum: [1, 2], description: '性别' },
strain: { type: 'string', description: '品系' },
varieties: { type: 'integer', description: '品种ID' },
cate: { type: 'integer', enum: [1, 2, 3, 4, 5, 6], description: '类别' },
weight: { type: 'number', description: '当前重量kg' },
penId: { type: 'integer', description: '栏舍ID' },
batchId: { type: 'integer', description: '批次ID' },
event: { type: 'string', description: '事件记录' },
eventTime: { type: 'integer', description: '事件时间(时间戳)' },
isVaccin: { type: 'integer', enum: [0, 1], description: '是否疫苗' },
isInsemination: { type: 'integer', enum: [0, 1], description: '是否配种' },
isInsure: { type: 'integer', enum: [0, 1], description: '是否保险' },
isMortgage: { type: 'integer', enum: [0, 1], description: '是否抵押' }
}
}
}
}
},
responses: {
'200': {
description: '更新成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '动物信息更新成功' },
data: { $ref: '#/components/schemas/Animal' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '动物不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
},
delete: {
tags: ['动物管理'],
summary: '删除动物档案',
description: '删除指定动物档案(软删除)',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '动物ID'
}
],
responses: {
'200': {
description: '删除成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '动物档案删除成功' }
}
}
}
}
},
'404': {
description: '动物不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
}
},
// 动物健康记录
'/animals/{id}/health': {
get: {
tags: ['动物管理'],
summary: '获取动物健康记录',
description: '获取指定动物的健康记录',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '动物ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
animalId: { type: 'integer' },
checkDate: { type: 'string', format: 'date' },
temperature: { type: 'number' },
weight: { type: 'number' },
healthStatus: { type: 'string' },
symptoms: { type: 'string' },
treatment: { type: 'string' },
veterinarian: { type: 'string' },
notes: { type: 'string' }
}
}
}
}
}
}
}
}
}
},
post: {
tags: ['动物管理'],
summary: '添加动物健康记录',
description: '为指定动物添加健康检查记录',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '动物ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['checkDate', 'healthStatus'],
properties: {
checkDate: { type: 'string', format: 'date', description: '检查日期' },
temperature: { type: 'number', description: '体温' },
weight: { type: 'number', description: '体重' },
healthStatus: {
type: 'string',
enum: ['healthy', 'sick', 'recovering', 'quarantine'],
description: '健康状态'
},
symptoms: { type: 'string', description: '症状描述' },
treatment: { type: 'string', description: '治疗方案' },
veterinarian: { type: 'string', description: '兽医姓名' },
notes: { type: 'string', description: '备注' }
}
}
}
}
},
responses: {
'201': {
description: '添加成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '健康记录添加成功' }
}
}
}
}
}
}
}
},
// 动物繁殖记录
'/animals/{id}/breeding': {
get: {
tags: ['动物管理'],
summary: '获取动物繁殖记录',
description: '获取指定动物的繁殖记录',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '动物ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
animalId: { type: 'integer' },
breedingDate: { type: 'string', format: 'date' },
matingType: { type: 'string', enum: ['natural', 'artificial'] },
sireId: { type: 'integer' },
expectedCalvingDate: { type: 'string', format: 'date' },
actualCalvingDate: { type: 'string', format: 'date' },
calvingResult: { type: 'string' },
offspringCount: { type: 'integer' },
notes: { type: 'string' }
}
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
}
}
};
// 动物数据模型
const animalSchemas = {
// 动物管理相关的数据模型定义
const animalsSchemas = {
Animal: {
type: 'object',
properties: {
id: { type: 'integer', description: '动物ID' },
orgId: { type: 'integer', description: '组织ID' },
earNumber: { type: 'string', description: '耳标号' },
sex: {
type: 'integer',
enum: [1, 2],
description: '性别1-公牛2-母牛'
},
strain: { type: 'string', description: '品系' },
varieties: { type: 'integer', description: '品种ID' },
cate: {
type: 'integer',
enum: [1, 2, 3, 4, 5, 6],
description: '类别1-犊牛2-育成母牛3-架子牛4-青年牛5-基础母牛6-育肥牛'
},
birthWeight: { type: 'number', description: '出生重量kg' },
birthday: { type: 'integer', description: '出生日期(时间戳)' },
penId: { type: 'integer', description: '栏舍ID' },
intoTime: { type: 'integer', description: '入场时间(时间戳)' },
parity: { type: 'integer', description: '胎次' },
source: {
type: 'integer',
enum: [1, 2, 3, 4, 5],
description: '来源1-合作社2-农户3-养殖场4-进口5-自繁'
},
sourceDay: { type: 'integer', description: '来源日龄' },
sourceWeight: { type: 'number', description: '来源重量kg' },
weight: { type: 'number', description: '当前重量kg' },
event: { type: 'string', description: '事件记录' },
eventTime: { type: 'integer', description: '事件时间(时间戳)' },
lactationDay: { type: 'integer', description: '泌乳天数' },
semenNum: { type: 'string', description: '精液编号' },
isWear: { type: 'integer', enum: [0, 1], description: '是否佩戴设备' },
batchId: { type: 'integer', description: '批次ID' },
imgs: { type: 'string', description: '图片URL多个用逗号分隔' },
isEleAuth: { type: 'integer', enum: [0, 1], description: '是否电子认证' },
isQuaAuth: { type: 'integer', enum: [0, 1], description: '是否质量认证' },
isDelete: { type: 'integer', enum: [0, 1], description: '是否删除' },
isOut: { type: 'integer', enum: [0, 1], description: '是否出栏' },
createUid: { type: 'integer', description: '创建用户ID' },
createTime: { type: 'integer', description: '创建时间(时间戳)' },
algebra: { type: 'string', description: '代数' },
colour: { type: 'string', description: '毛色' },
infoWeight: { type: 'number', description: '信息重量' },
descent: { type: 'string', description: '血统' },
isVaccin: { type: 'integer', enum: [0, 1], description: '是否疫苗' },
isInsemination: { type: 'integer', enum: [0, 1], description: '是否配种' },
isInsure: { type: 'integer', enum: [0, 1], description: '是否保险' },
isMortgage: { type: 'integer', enum: [0, 1], description: '是否抵押' },
updateTime: { type: 'integer', description: '更新时间(时间戳)' },
breedBullTime: { type: 'integer', description: '配种时间(时间戳)' },
level: { type: 'string', description: '等级' },
sixWeight: { type: 'number', description: '6月龄重量' },
eighteenWeight: { type: 'number', description: '18月龄重量' },
twelveDayWeight: { type: 'number', description: '12日龄重量' },
eighteenDayWeight: { type: 'number', description: '18日龄重量' },
xxivDayWeight: { type: 'number', description: '24日龄重量' },
semenBreedImgs: { type: 'string', description: '配种图片' },
sellStatus: { type: 'integer', description: '销售状态' },
weightCalculateTime: { type: 'integer', description: '重量计算时间' },
dayOfBirthday: { type: 'integer', description: '出生天数' },
userId: { type: 'integer', description: '用户ID' }
collarNumber: { type: 'string', description: '项圈编号' },
name: { type: 'string', description: '动物名称' },
breed: { type: 'string', description: '品种' },
sex: { type: 'string', enum: ['公', '母'], description: '性别' },
birthDate: { type: 'string', format: 'date', description: '出生日期' },
farmId: { type: 'integer', description: '养殖场ID' },
penId: { type: 'integer', description: '圈舍ID' },
status: { type: 'string', description: '状态' },
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
}
},
AnimalBindingInfo: {
type: 'object',
properties: {
basicInfo: { type: 'object', description: '基础信息' },
birthInfo: { type: 'object', description: '出生信息' },
pedigreeInfo: { type: 'object', description: '族谱信息' },
insuranceInfo: { type: 'object', description: '保险信息' },
loanInfo: { type: 'object', description: '贷款信息' },
deviceInfo: { type: 'object', description: '设备信息' },
farmInfo: { type: 'object', description: '农场信息' }
}
}
};
module.exports = { animalsPaths, animalSchemas };
module.exports = {
animalsPaths,
animalsSchemas
};

View File

@@ -1,307 +1,89 @@
/**
* 用户认证模块 Swagger 文档
* 认证模块 Swagger 文档
* @file swagger-auth.js
* @description 用户认证相关的 Swagger API 文档定义
*/
// 认证相关的 API 路径定义
const authPaths = {
// 用户登录
'/auth/login': {
'/api/auth/login': {
post: {
tags: ['用户认证'],
summary: '用户登录',
description: '用户通过用户名/邮箱和密码登录系统',
security: [], // 登录接口不需要认证
tags: ['用户认证'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['username', 'password'],
properties: {
username: {
type: 'string',
description: '用户名或邮箱',
example: 'admin'
},
password: {
type: 'string',
description: '密码',
example: '123456'
}
}
}
schema: { $ref: '#/components/schemas/LoginRequest' }
}
}
},
responses: {
'200': {
description: '登录成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '登录成功' },
token: { type: 'string', description: 'JWT Token' },
user: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
avatar: { type: 'string' },
status: { type: 'string' },
roles: { type: 'array', items: { type: 'object' } }
}
}
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'401': {
description: '用户名或密码错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'429': {
description: '登录尝试次数过多,请稍后再试',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
401: { $ref: '#/components/responses/Unauthorized' }
}
}
},
// 用户注册
'/auth/register': {
'/api/auth/register': {
post: {
tags: ['用户认证'],
summary: '用户注册',
description: '用户注册账号',
security: [], // 注册接口不需要认证
tags: ['用户认证'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['username', 'email', 'password'],
properties: {
username: {
type: 'string',
description: '用户名',
example: 'newuser'
},
email: {
type: 'string',
format: 'email',
description: '邮箱地址',
example: 'newuser@example.com'
},
password: {
type: 'string',
minLength: 6,
description: '密码至少6位',
example: '123456'
},
phone: {
type: 'string',
description: '手机号码',
example: '13800138000'
}
}
}
schema: { $ref: '#/components/schemas/RegisterRequest' }
}
}
},
responses: {
'201': {
description: '注册成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '注册成功' },
user: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' }
}
}
}
}
}
}
},
'400': {
description: '请求参数错误或用户已存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
201: { $ref: '#/components/responses/Created' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
},
// 获取当前用户信息
'/auth/me': {
'/api/auth/me': {
get: {
tags: ['用户认证'],
summary: '获取当前用户信息',
description: '获取当前登录用户的详细信息',
tags: ['用户认证'],
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
avatar: { type: 'string' },
status: { type: 'string' },
roles: { type: 'array', items: { type: 'object' } },
permissions: { type: 'array', items: { type: 'string' } },
menus: { type: 'array', items: { type: 'object' } }
}
}
}
}
}
}
},
'401': {
description: '未授权Token无效或已过期',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
401: { $ref: '#/components/responses/Unauthorized' }
}
}
},
// Token验证
'/auth/validate': {
'/api/auth/validate': {
get: {
summary: '验证Token有效性',
tags: ['用户认证'],
summary: 'Token验证',
description: '验证当前Token是否有效',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Token有效',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: 'Token有效' },
user: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
email: { type: 'string' }
}
}
}
}
}
}
},
'401': {
description: 'Token无效或已过期',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
401: { $ref: '#/components/responses/Unauthorized' }
}
}
},
// 获取所有角色
'/auth/roles': {
'/api/auth/roles': {
get: {
tags: ['用户认证'],
summary: '获取所有角色',
description: '获取系统中所有可用的角色列表',
tags: ['用户认证'],
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
description: { type: 'string' },
permissions: { type: 'array', items: { type: 'string' } }
}
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
// 为用户分配角色
'/auth/users/{userId}/roles': {
'/api/auth/users/{userId}/roles': {
post: {
tags: ['用户认证'],
summary: '为用户分配角色',
description: '为指定用户分配一个或多个角色',
tags: ['用户认证'],
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'userId',
in: 'path',
required: true,
{
name: 'userId',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '用户ID'
}
@@ -312,101 +94,108 @@ const authPaths = {
'application/json': {
schema: {
type: 'object',
required: ['roleIds'],
required: ['roleId'],
properties: {
roleIds: {
type: 'array',
items: { type: 'integer' },
description: '角色ID列表'
}
roleId: { type: 'integer', description: '角色ID' }
}
}
}
}
},
responses: {
'200': {
description: '分配成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '角色分配成功' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '用户不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
400: { $ref: '#/components/responses/BadRequest' },
403: { $ref: '#/components/responses/Forbidden' }
}
}
},
// 移除用户角色
'/auth/users/{userId}/roles/{roleId}': {
'/api/auth/users/{userId}/roles/{roleId}': {
delete: {
summary: '移除用户的角色',
tags: ['用户认证'],
summary: '移除用户角色',
description: '移除用户的指定角色',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'userId',
in: 'path',
required: true,
{
name: 'userId',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '用户ID'
},
{
name: 'roleId',
in: 'path',
required: true,
{
name: 'roleId',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '角色ID'
}
],
responses: {
'200': {
description: '移除成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '角色移除成功' }
}
}
}
}
},
'404': {
description: '用户或角色不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
}
}
};
module.exports = authPaths;
// 认证相关的数据模型定义
const authSchemas = {
LoginRequest: {
type: 'object',
required: ['username', 'password'],
properties: {
username: { type: 'string', description: '用户名或邮箱' },
password: { type: 'string', format: 'password', description: '密码' }
}
},
LoginResponse: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
token: { type: 'string', description: 'JWT令牌' },
user: { $ref: '#/components/schemas/User' },
permissions: {
type: 'array',
items: { type: 'string' },
description: '用户权限列表'
},
accessibleMenus: {
type: 'array',
items: { type: 'string' },
description: '可访问的菜单列表'
}
}
},
RegisterRequest: {
type: 'object',
required: ['username', 'email', 'password'],
properties: {
username: { type: 'string', description: '用户名' },
email: { type: 'string', format: 'email', description: '邮箱地址' },
password: { type: 'string', format: 'password', description: '密码' }
}
},
RegisterResponse: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
user: { $ref: '#/components/schemas/User' }
}
},
Role: {
type: 'object',
properties: {
id: { type: 'integer', description: '角色ID' },
name: { type: 'string', description: '角色名称' },
description: { type: 'string', description: '角色描述' }
}
}
};
module.exports = {
authPaths,
authSchemas
};

View File

@@ -1,223 +0,0 @@
/**
* 完整版Swagger配置
* @file swagger-complete.js
* @description 宁夏智慧养殖监管平台完整API文档配置
*/
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '宁夏智慧养殖监管平台 API',
version: '2.0.0',
description: `
宁夏智慧养殖监管平台API文档
## 功能模块
- **用户认证**: 登录、注册、权限验证
- **用户管理**: 用户CRUD操作、角色管理
- **养殖场管理**: 养殖场信息管理
- **动物管理**: 牲畜信息管理、批次管理
- **设备管理**: IoT设备管理、智能设备
- **预警系统**: 智能预警、告警处理
- **围栏管理**: 电子围栏设置
- **数据统计**: 各类统计报表
- **系统管理**: 系统配置、备份等
## 认证方式
使用JWT Token进行身份认证请在请求头中添加
\`Authorization: Bearer <token>\`
`,
contact: {
name: '开发团队',
email: 'dev@nxxm.com'
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT'
}
},
servers: [
{
url: 'http://localhost:5350/api',
description: '开发环境'
},
{
url: 'https://api.nxxm.com/api',
description: '生产环境'
}
],
tags: [
{
name: '用户认证',
description: '用户登录、注册、权限验证相关接口'
},
{
name: '用户管理',
description: '用户信息管理、角色权限管理'
},
{
name: '养殖场管理',
description: '养殖场信息的增删改查'
},
{
name: '动物管理',
description: '牲畜信息管理、批次管理、转移记录'
},
{
name: '圈舍管理',
description: '圈舍信息管理、牲畜圈舍分配'
},
{
name: '设备管理',
description: 'IoT设备管理、设备绑定、状态监控'
},
{
name: '智能设备',
description: '智能耳标、智能项圈等设备管理'
},
{
name: '预警系统',
description: '智能预警、告警处理、预警统计'
},
{
name: '电子围栏',
description: '电子围栏设置、围栏点管理'
},
{
name: '地图服务',
description: '地图相关功能、位置服务'
},
{
name: '数据统计',
description: '各类统计数据、报表生成'
},
{
name: '报表管理',
description: '报表生成、导出功能'
},
{
name: '系统管理',
description: '系统配置、菜单管理、权限配置'
},
{
name: '备份管理',
description: '数据备份、恢复功能'
},
{
name: '操作日志',
description: '系统操作日志记录和查询'
},
{
name: '产品管理',
description: '产品信息管理'
},
{
name: '订单管理',
description: '订单处理、订单查询'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'JWT认证格式Bearer <token>'
}
},
schemas: {
// 通用响应格式
ApiResponse: {
type: 'object',
properties: {
success: {
type: 'boolean',
description: '请求是否成功'
},
message: {
type: 'string',
description: '响应消息'
},
data: {
description: '响应数据'
},
total: {
type: 'integer',
description: '总记录数(分页时使用)'
},
page: {
type: 'integer',
description: '当前页码'
},
limit: {
type: 'integer',
description: '每页记录数'
}
}
},
// 错误响应
ErrorResponse: {
type: 'object',
properties: {
success: {
type: 'boolean',
example: false
},
message: {
type: 'string',
description: '错误消息'
},
error: {
type: 'string',
description: '错误详情'
}
}
},
// 分页参数
PaginationQuery: {
type: 'object',
properties: {
page: {
type: 'integer',
minimum: 1,
default: 1,
description: '页码'
},
limit: {
type: 'integer',
minimum: 1,
maximum: 100,
default: 10,
description: '每页记录数'
},
search: {
type: 'string',
description: '搜索关键词'
}
}
}
}
},
security: [
{
bearerAuth: []
}
]
},
apis: [
'./routes/*.js',
'./controllers/*.js'
]
};
const specs = swaggerJSDoc(options);
// 手动添加API路径定义
if (!specs.paths) {
specs.paths = {};
}
module.exports = specs;

View File

@@ -1,369 +0,0 @@
/**
* Swagger API文档配置
* @file swagger-config.js
* @description 配置Swagger API文档包含智能耳标预警和智能项圈预警接口
*/
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '智能预警系统 API',
version: '1.0.0',
description: '智能耳标预警和智能项圈预警系统API文档',
contact: {
name: '开发团队',
email: 'dev@example.com'
}
},
servers: [
{
url: 'http://localhost:5350/api',
description: '开发环境'
}
],
tags: [
{
name: '智能耳标预警',
description: '智能耳标预警相关接口'
},
{
name: '智能项圈预警',
description: '智能项圈预警相关接口'
}
],
components: {
schemas: {
EartagAlert: {
type: 'object',
properties: {
id: {
type: 'string',
description: '预警ID',
example: '123_offline'
},
deviceId: {
type: 'integer',
description: '设备ID',
example: 123
},
deviceName: {
type: 'string',
description: '设备名称',
example: 'EARTAG001'
},
eartagNumber: {
type: 'string',
description: '耳标编号',
example: 'EARTAG001'
},
alertType: {
type: 'string',
description: '预警类型',
enum: ['battery', 'offline', 'temperature', 'movement'],
example: 'offline'
},
alertLevel: {
type: 'string',
description: '预警级别',
enum: ['high', 'medium', 'low'],
example: 'high'
},
alertTime: {
type: 'string',
format: 'date-time',
description: '预警时间',
example: '2024-01-15 10:30:00'
},
battery: {
type: 'integer',
description: '设备电量',
example: 85
},
temperature: {
type: 'number',
description: '设备温度',
example: 25.5
},
dailySteps: {
type: 'integer',
description: '当日步数',
example: 0
},
totalSteps: {
type: 'integer',
description: '总步数',
example: 1500
},
yesterdaySteps: {
type: 'integer',
description: '昨日步数',
example: 1500
},
deviceStatus: {
type: 'string',
description: '设备状态',
example: '离线'
},
gpsSignal: {
type: 'string',
description: 'GPS信号',
example: '无'
},
movementStatus: {
type: 'string',
description: '运动状态',
example: '静止'
},
description: {
type: 'string',
description: '预警描述',
example: '设备已离线超过30分钟'
},
longitude: {
type: 'number',
description: '经度',
example: 116.3974
},
latitude: {
type: 'number',
description: '纬度',
example: 39.9093
}
}
},
CollarAlert: {
type: 'object',
properties: {
id: {
type: 'string',
description: '预警ID',
example: '123_offline'
},
deviceId: {
type: 'integer',
description: '设备ID',
example: 123
},
deviceName: {
type: 'string',
description: '设备名称',
example: 'COLLAR001'
},
collarNumber: {
type: 'string',
description: '项圈编号',
example: 'COLLAR001'
},
alertType: {
type: 'string',
description: '预警类型',
enum: ['battery', 'offline', 'temperature', 'movement', 'wear'],
example: 'offline'
},
alertLevel: {
type: 'string',
description: '预警级别',
enum: ['high', 'medium', 'low'],
example: 'high'
},
alertTime: {
type: 'string',
format: 'date-time',
description: '预警时间',
example: '2024-01-15 10:30:00'
},
battery: {
type: 'integer',
description: '设备电量',
example: 85
},
temperature: {
type: 'number',
description: '设备温度',
example: 25.5
},
dailySteps: {
type: 'integer',
description: '当日步数',
example: 0
},
totalSteps: {
type: 'integer',
description: '总步数',
example: 1500
},
yesterdaySteps: {
type: 'integer',
description: '昨日步数',
example: 1500
},
deviceStatus: {
type: 'string',
description: '设备状态',
example: '离线'
},
gpsSignal: {
type: 'string',
description: 'GPS信号',
example: '无'
},
wearStatus: {
type: 'string',
description: '佩戴状态',
example: '未佩戴'
},
movementStatus: {
type: 'string',
description: '运动状态',
example: '静止'
},
description: {
type: 'string',
description: '预警描述',
example: '设备已离线超过30分钟'
},
longitude: {
type: 'number',
description: '经度',
example: 116.3974
},
latitude: {
type: 'number',
description: '纬度',
example: 39.9093
}
}
},
AlertStats: {
type: 'object',
properties: {
totalDevices: {
type: 'integer',
description: '设备总数',
example: 150
},
lowBattery: {
type: 'integer',
description: '低电量预警数量',
example: 12
},
offline: {
type: 'integer',
description: '离线预警数量',
example: 8
},
highTemperature: {
type: 'integer',
description: '高温预警数量',
example: 5
},
lowTemperature: {
type: 'integer',
description: '低温预警数量',
example: 3
},
abnormalMovement: {
type: 'integer',
description: '异常运动预警数量',
example: 7
},
wearOff: {
type: 'integer',
description: '项圈脱落预警数量(仅项圈)',
example: 2
},
totalAlerts: {
type: 'integer',
description: '预警总数',
example: 35
}
}
},
ApiResponse: {
type: 'object',
properties: {
success: {
type: 'boolean',
description: '请求是否成功',
example: true
},
data: {
type: 'object',
description: '响应数据'
},
message: {
type: 'string',
description: '响应消息',
example: '操作成功'
},
total: {
type: 'integer',
description: '数据总数(分页时使用)',
example: 100
},
stats: {
$ref: '#/components/schemas/AlertStats'
},
pagination: {
type: 'object',
properties: {
page: {
type: 'integer',
description: '当前页码',
example: 1
},
limit: {
type: 'integer',
description: '每页数量',
example: 10
},
total: {
type: 'integer',
description: '总数据量',
example: 100
},
pages: {
type: 'integer',
description: '总页数',
example: 10
}
}
}
}
},
ErrorResponse: {
type: 'object',
properties: {
success: {
type: 'boolean',
description: '请求是否成功',
example: false
},
message: {
type: 'string',
description: '错误消息',
example: '请求失败'
},
error: {
type: 'string',
description: '详细错误信息',
example: '具体错误描述'
}
}
}
}
}
},
apis: [
'./routes/smart-alerts.js',
'./controllers/smartEartagAlertController.js',
'./controllers/smartCollarAlertController.js'
]
};
const specs = swaggerJSDoc(options);
module.exports = specs;

View File

@@ -1,684 +1,131 @@
/**
* 设备管理模块 Swagger 文档
* @file swagger-devices.js
* @description 设备管理相关的 Swagger API 文档定义
*/
// 设备管理相关的 API 路径定义
const devicesPaths = {
// 获取所有设备
'/devices': {
'/api/devices': {
get: {
summary: '获取所有设备',
tags: ['设备管理'],
summary: '获取设备列表',
description: '分页获取系统中的所有设备信息',
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词(设备名称、设备编号)'
},
{
name: 'type',
in: 'query',
schema: { type: 'string' },
description: '设备类型筛选'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['online', 'offline', 'maintenance', 'error'] },
description: '设备状态筛选'
},
{
name: 'farmId',
in: 'query',
schema: { type: 'integer' },
description: '养殖场ID筛选'
}
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Device' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
},
post: {
summary: '创建设备',
tags: ['设备管理'],
summary: '创建新设备',
description: '添加新的设备到系统中',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'type', 'farmId'],
properties: {
name: { type: 'string', description: '设备名称' },
type: {
type: 'string',
enum: ['collar', 'ear_tag', 'temperature_sensor', 'humidity_sensor', 'camera', 'feeder', 'water_dispenser'],
description: '设备类型'
},
deviceNumber: { type: 'string', description: '设备编号' },
model: { type: 'string', description: '设备型号' },
manufacturer: { type: 'string', description: '制造商' },
status: {
type: 'string',
enum: ['online', 'offline', 'maintenance', 'error'],
default: 'offline',
description: '设备状态'
},
farmId: { type: 'integer', description: '所属养殖场ID' },
location: { type: 'string', description: '设备位置' },
installationDate: { type: 'string', format: 'date', description: '安装日期' },
lastMaintenance: { type: 'string', format: 'date', description: '最近维护时间' },
batteryLevel: { type: 'number', minimum: 0, maximum: 100, description: '电池电量(%' },
firmwareVersion: { type: 'string', description: '固件版本' },
specifications: { type: 'object', description: '设备规格参数' },
notes: { type: 'string', description: '备注' }
}
}
schema: { $ref: '#/components/schemas/DeviceInput' }
}
}
},
responses: {
'201': {
description: '创建成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '设备创建成功' },
data: { $ref: '#/components/schemas/Device' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
201: { $ref: '#/components/responses/Created' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
},
// 公共设备数据
'/devices/public': {
'/api/devices/{id}': {
get: {
summary: '根据ID获取设备',
tags: ['设备管理'],
summary: '获取公共设备数据',
description: '获取可公开访问的设备基本信息',
security: [], // 公共接口不需要认证
parameters: [
{
name: 'type',
in: 'query',
schema: { type: 'string' },
description: '设备类型筛选'
},
{
name: 'status',
in: 'query',
schema: { type: 'string' },
description: '设备状态筛选'
}
],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
type: { type: 'string' },
status: { type: 'string' },
location: { type: 'string' }
}
}
}
}
}
}
}
}
}
}
},
// 搜索设备
'/devices/search': {
get: {
tags: ['设备管理'],
summary: '搜索设备',
description: '根据设备名称搜索设备',
parameters: [
{
name: 'name',
in: 'query',
required: true,
schema: { type: 'string' },
description: '设备名称关键词'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '返回结果数量限制'
}
],
responses: {
'200': {
description: '搜索成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Device' }
}
}
}
}
}
}
}
}
},
// 设备统计 - 按状态
'/devices/stats/status': {
get: {
tags: ['设备管理'],
summary: '按状态统计设备数量',
description: '获取不同状态下的设备数量统计',
responses: {
'200': {
description: '统计成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
status: { type: 'string', example: 'online' },
count: { type: 'integer', example: 25 },
percentage: { type: 'number', example: 62.5 }
}
}
}
}
}
}
}
}
}
}
},
// 设备统计 - 按类型
'/devices/stats/type': {
get: {
tags: ['设备管理'],
summary: '按类型统计设备数量',
description: '获取不同类型设备的数量统计',
responses: {
'200': {
description: '统计成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string', example: 'collar' },
typeName: { type: 'string', example: '智能项圈' },
count: { type: 'integer', example: 15 },
percentage: { type: 'number', example: 37.5 }
}
}
}
}
}
}
}
}
}
}
},
// 获取指定设备详情
'/devices/{id}': {
get: {
tags: ['设备管理'],
summary: '获取设备详情',
description: '根据设备ID获取详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '设备ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: { $ref: '#/components/schemas/Device' }
}
}
}
}
},
'404': {
description: '设备不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
put: {
tags: ['设备管理'],
summary: '更新设备信息',
description: '更新指定设备的信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '设备ID'
}
],
tags: ['设备管理'],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: { type: 'string', description: '设备名称' },
type: { type: 'string', description: '设备类型' },
deviceNumber: { type: 'string', description: '设备编号' },
model: { type: 'string', description: '设备型号' },
manufacturer: { type: 'string', description: '制造商' },
status: {
type: 'string',
enum: ['online', 'offline', 'maintenance', 'error'],
description: '设备状态'
},
farmId: { type: 'integer', description: '所属养殖场ID' },
location: { type: 'string', description: '设备位置' },
lastMaintenance: { type: 'string', format: 'date', description: '最近维护时间' },
batteryLevel: { type: 'number', minimum: 0, maximum: 100, description: '电池电量(%' },
firmwareVersion: { type: 'string', description: '固件版本' },
specifications: { type: 'object', description: '设备规格参数' },
notes: { type: 'string', description: '备注' }
}
}
schema: { $ref: '#/components/schemas/DeviceInput' }
}
}
},
responses: {
'200': {
description: '更新成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '设备信息更新成功' },
data: { $ref: '#/components/schemas/Device' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '设备不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
delete: {
tags: ['设备管理'],
summary: '删除设备',
description: '删除指定设备(软删除)',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '设备ID'
}
],
tags: ['设备管理'],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '删除成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '设备删除成功' }
}
}
}
}
},
'404': {
description: '设备不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
}
},
// 设备维护记录
'/devices/{id}/maintenance': {
'/api/devices/stats/status': {
get: {
summary: '获取设备状态统计',
tags: ['设备管理'],
summary: '获取设备维护记录',
description: '获取指定设备的维护记录',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '设备ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
deviceId: { type: 'integer' },
maintenanceDate: { type: 'string', format: 'date' },
maintenanceType: { type: 'string', enum: ['routine', 'repair', 'upgrade'] },
description: { type: 'string' },
technician: { type: 'string' },
cost: { type: 'number' },
notes: { type: 'string' }
}
}
}
}
}
}
}
}
}
},
post: {
tags: ['设备管理'],
summary: '添加设备维护记录',
description: '为指定设备添加维护记录',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '设备ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['maintenanceDate', 'maintenanceType', 'description'],
properties: {
maintenanceDate: { type: 'string', format: 'date', description: '维护日期' },
maintenanceType: {
type: 'string',
enum: ['routine', 'repair', 'upgrade'],
description: '维护类型'
},
description: { type: 'string', description: '维护描述' },
technician: { type: 'string', description: '技术员姓名' },
cost: { type: 'number', description: '维护费用' },
notes: { type: 'string', description: '备注' }
}
}
}
}
},
responses: {
'201': {
description: '添加成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '维护记录添加成功' }
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
},
// 设备数据监控
'/devices/{id}/data': {
'/api/devices/stats/type': {
get: {
summary: '获取设备类型统计',
tags: ['设备管理'],
summary: '获取设备监控数据',
description: '获取指定设备的实时监控数据',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '设备ID'
},
{
name: 'startTime',
in: 'query',
schema: { type: 'string', format: 'date-time' },
description: '开始时间'
},
{
name: 'endTime',
in: 'query',
schema: { type: 'string', format: 'date-time' },
description: '结束时间'
},
{
name: 'dataType',
in: 'query',
schema: { type: 'string', enum: ['temperature', 'humidity', 'location', 'battery', 'activity'] },
description: '数据类型'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
timestamp: { type: 'string', format: 'date-time' },
dataType: { type: 'string' },
value: { type: 'number' },
unit: { type: 'string' },
location: {
type: 'object',
properties: {
latitude: { type: 'number' },
longitude: { type: 'number' }
}
}
}
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
}
};
// 设备数据模型
const deviceSchemas = {
// 设备管理相关的数据模型定义
const devicesSchemas = {
Device: {
type: 'object',
properties: {
id: { type: 'integer', description: '设备ID' },
deviceId: { type: 'string', description: '设备编号' },
name: { type: 'string', description: '设备名称' },
type: {
type: 'string',
enum: ['collar', 'ear_tag', 'temperature_sensor', 'humidity_sensor', 'camera', 'feeder', 'water_dispenser'],
description: '设备类型'
},
deviceNumber: { type: 'string', description: '设备编号' },
model: { type: 'string', description: '设备型号' },
manufacturer: { type: 'string', description: '制造商' },
status: {
type: 'string',
enum: ['online', 'offline', 'maintenance', 'error'],
description: '设备状态'
},
farmId: { type: 'integer', description: '所属养殖场ID' },
farmName: { type: 'string', description: '养殖场名称' },
location: { type: 'string', description: '设备位置' },
installationDate: { type: 'string', format: 'date', description: '安装日期' },
lastMaintenance: { type: 'string', format: 'date', description: '最近维护时间' },
batteryLevel: { type: 'number', minimum: 0, maximum: 100, description: '电池电量(%' },
firmwareVersion: { type: 'string', description: '固件版本' },
specifications: {
type: 'object',
description: '设备规格参数',
additionalProperties: true
},
lastDataTime: { type: 'string', format: 'date-time', description: '最后数据时间' },
isActive: { type: 'boolean', description: '是否激活' },
notes: { type: 'string', description: '备注' },
type: { type: 'string', description: '设备类型' },
status: { type: 'string', enum: ['online', 'offline', 'fault'], description: '设备状态' },
batteryLevel: { type: 'integer', description: '电池电量' },
location: { type: 'string', description: '位置信息' },
farmId: { type: 'integer', description: '养殖场ID' },
animalId: { type: 'integer', description: '绑定的动物ID' },
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
}
},
DeviceInput: {
type: 'object',
required: ['deviceId', 'name', 'type'],
properties: {
deviceId: { type: 'string', description: '设备编号' },
name: { type: 'string', description: '设备名称' },
type: { type: 'string', description: '设备类型' },
farmId: { type: 'integer', description: '养殖场ID' },
animalId: { type: 'integer', description: '绑定的动物ID' }
}
}
};
module.exports = { devicesPaths, deviceSchemas };
module.exports = {
devicesPaths,
devicesSchemas
};

View File

@@ -1,623 +1,124 @@
/**
* 养殖场管理模块 Swagger 文档
* @file swagger-farms.js
* @description 养殖场管理相关的 Swagger API 文档定义
*/
// 养殖场管理相关的 API 路径定义
const farmsPaths = {
// 获取所有养殖场
'/farms': {
'/api/farms': {
get: {
summary: '获取所有养殖场',
tags: ['养殖场管理'],
summary: '获取养殖场列表',
description: '分页获取系统中的所有养殖场',
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词(养殖场名称、地址)'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['active', 'inactive', 'suspended'] },
description: '养殖场状态筛选'
},
{
name: 'type',
in: 'query',
schema: { type: 'string', enum: ['cattle', 'sheep', 'pig', 'poultry'] },
description: '养殖类型筛选'
}
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Farm' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
},
post: {
tags: ['养殖场管理'],
summary: '创建新养殖场',
description: '创建新的养殖场记录',
tags: ['养殖场管理'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['name', 'address', 'type', 'ownerId'],
properties: {
name: { type: 'string', description: '养殖场名称' },
address: { type: 'string', description: '养殖场地址' },
type: {
type: 'string',
enum: ['cattle', 'sheep', 'pig', 'poultry'],
description: '养殖类型cattle-牛sheep-羊pig-猪poultry-家禽'
},
ownerId: { type: 'integer', description: '养殖场主ID' },
description: { type: 'string', description: '养殖场描述' },
area: { type: 'number', description: '养殖场面积(平方米)' },
capacity: { type: 'integer', description: '最大养殖容量' },
contactPhone: { type: 'string', description: '联系电话' },
contactEmail: { type: 'string', format: 'email', description: '联系邮箱' },
coordinates: {
type: 'object',
properties: {
latitude: { type: 'number', description: '纬度' },
longitude: { type: 'number', description: '经度' }
},
description: '地理坐标'
},
status: {
type: 'string',
enum: ['active', 'inactive', 'suspended'],
default: 'active',
description: '养殖场状态'
}
}
}
schema: { $ref: '#/components/schemas/FarmInput' }
}
}
},
responses: {
'201': {
description: '创建成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '养殖场创建成功' },
data: { $ref: '#/components/schemas/Farm' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
201: { $ref: '#/components/responses/Created' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
},
// 搜索养殖场
'/farms/search': {
'/api/farms/{id}': {
get: {
summary: '根据ID获取养殖场',
tags: ['养殖场管理'],
summary: '搜索养殖场',
description: '根据名称、地址等关键词搜索养殖场',
parameters: [
{
name: 'q',
in: 'query',
required: true,
schema: { type: 'string' },
description: '搜索关键词'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '返回结果数量限制'
}
],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '搜索成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Farm' }
}
}
}
}
}
}
}
}
},
// 公共养殖场数据
'/farms/public': {
get: {
tags: ['养殖场管理'],
summary: '获取公共养殖场数据',
description: '获取可公开访问的养殖场基本信息',
security: [], // 公共接口不需要认证
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
type: { type: 'string' },
address: { type: 'string' },
area: { type: 'number' }
}
}
}
}
}
}
}
}
}
}
},
// 获取指定养殖场详情
'/farms/{id}': {
get: {
tags: ['养殖场管理'],
summary: '获取养殖场详情',
description: '根据养殖场ID获取详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '养殖场ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: { $ref: '#/components/schemas/Farm' }
}
}
}
}
},
'404': {
description: '养殖场不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
put: {
tags: ['养殖场管理'],
summary: '更新养殖场信息',
description: '更新指定养殖场的信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '养殖场ID'
}
],
tags: ['养殖场管理'],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: { type: 'string', description: '养殖场名称' },
address: { type: 'string', description: '养殖场地址' },
type: {
type: 'string',
enum: ['cattle', 'sheep', 'pig', 'poultry'],
description: '养殖类型'
},
description: { type: 'string', description: '养殖场描述' },
area: { type: 'number', description: '养殖场面积(平方米)' },
capacity: { type: 'integer', description: '最大养殖容量' },
contactPhone: { type: 'string', description: '联系电话' },
contactEmail: { type: 'string', format: 'email', description: '联系邮箱' },
coordinates: {
type: 'object',
properties: {
latitude: { type: 'number', description: '纬度' },
longitude: { type: 'number', description: '经度' }
},
description: '地理坐标'
},
status: {
type: 'string',
enum: ['active', 'inactive', 'suspended'],
description: '养殖场状态'
}
}
}
schema: { $ref: '#/components/schemas/FarmInput' }
}
}
},
responses: {
'200': {
description: '更新成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '养殖场信息更新成功' },
data: { $ref: '#/components/schemas/Farm' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '养殖场不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
delete: {
tags: ['养殖场管理'],
summary: '删除养殖场',
description: '删除指定养殖场(软删除)',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '养殖场ID'
}
],
tags: ['养殖场管理'],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '删除成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '养殖场删除成功' }
}
}
}
}
},
'404': {
description: '养殖场不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
}
},
// 获取养殖场的动物列表
'/farms/{id}/animals': {
'/api/farms/search': {
get: {
summary: '搜索养殖场',
tags: ['养殖场管理'],
summary: '获取养殖场的动物列表',
description: '获取指定养殖场的所有动物',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '养殖场ID'
},
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['healthy', 'sick', 'quarantine', 'sold'] },
description: '动物状态筛选'
}
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Animal' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
}
}
},
'404': {
description: '养殖场不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
}
},
// 获取养殖场的设备列表
'/farms/{id}/devices': {
get: {
tags: ['养殖场管理'],
summary: '获取养殖场的设备列表',
description: '获取指定养殖场的所有设备',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '养殖场ID'
},
{
name: 'type',
in: 'query',
schema: { type: 'string', enum: ['sensor', 'camera', 'feeder', 'monitor'] },
description: '设备类型筛选'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['online', 'offline', 'maintenance'] },
description: '设备状态筛选'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/Device' }
}
}
}
}
}
},
'404': {
description: '养殖场不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
}
},
// 获取养殖场统计信息
'/farms/{id}/statistics': {
get: {
tags: ['养殖场管理'],
summary: '获取养殖场统计信息',
description: '获取指定养殖场的统计数据',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '养殖场ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'object',
properties: {
totalAnimals: { type: 'integer', description: '动物总数' },
healthyAnimals: { type: 'integer', description: '健康动物数' },
sickAnimals: { type: 'integer', description: '患病动物数' },
totalDevices: { type: 'integer', description: '设备总数' },
onlineDevices: { type: 'integer', description: '在线设备数' },
offlineDevices: { type: 'integer', description: '离线设备数' },
alertsCount: { type: 'integer', description: '预警数量' },
utilizationRate: { type: 'number', description: '利用率(%' }
}
}
}
}
}
}
},
'404': {
description: '养殖场不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
}
};
// 养殖场数据模型
const farmSchemas = {
// 养殖场管理相关的数据模型定义
const farmsSchemas = {
Farm: {
type: 'object',
properties: {
id: { type: 'integer', description: '养殖场ID' },
name: { type: 'string', description: '养殖场名称' },
address: { type: 'string', description: '养殖场地址' },
type: {
type: 'string',
enum: ['cattle', 'sheep', 'pig', 'poultry'],
description: '养殖类型cattle-牛sheep-羊pig-猪poultry-家禽'
},
description: { type: 'string', description: '养殖场描述' },
area: { type: 'number', description: '养殖场面积(平方米)' },
capacity: { type: 'integer', description: '最大养殖容量' },
currentCount: { type: 'integer', description: '当前动物数量' },
contactPhone: { type: 'string', description: '联系电话' },
contactEmail: { type: 'string', format: 'email', description: '联系邮箱' },
coordinates: {
type: 'object',
properties: {
latitude: { type: 'number', description: '纬度' },
longitude: { type: 'number', description: '经度' }
},
description: '地理坐标'
},
status: {
type: 'string',
enum: ['active', 'inactive', 'suspended'],
description: '养殖场状态active-活跃inactive-未激活suspended-暂停'
},
owner: {
type: 'object',
properties: {
id: { type: 'integer' },
username: { type: 'string' },
realName: { type: 'string' },
phone: { type: 'string' }
},
description: '养殖场主信息'
},
address: { type: 'string', description: '地址' },
contact: { type: 'string', description: '联系人' },
phone: { type: 'string', description: '联系电话' },
area: { type: 'number', description: '面积(平方米)' },
capacity: { type: 'integer', description: '容量(头)' },
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
}
},
FarmInput: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string', description: '养殖场名称' },
address: { type: 'string', description: '地址' },
contact: { type: 'string', description: '联系人' },
phone: { type: 'string', description: '联系电话' },
area: { type: 'number', description: '面积(平方米)' },
capacity: { type: 'integer', description: '容量(头)' }
}
}
};
module.exports = { farmsPaths, farmSchemas };
module.exports = {
farmsPaths,
farmsSchemas
};

View File

@@ -1,736 +1,147 @@
/**
* 报表管理模块 Swagger 文档
* @file swagger-reports.js
* @description 定义报表管理相关的API文档
* @description 报表管理相关的 Swagger API 文档定义
*/
/**
* @swagger
* tags:
* - name: 报表管理
* description: 报表生成、下载和管理
*/
// 报表管理相关的 API 路径定义
const reportsPaths = {
'/api/reports/farm': {
post: {
summary: '生成养殖统计报表',
tags: ['报表管理'],
security: [{ bearerAuth: [] }],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
startDate: { type: 'string', format: 'date', description: '开始日期' },
endDate: { type: 'string', format: 'date', description: '结束日期' },
farmIds: {
type: 'array',
items: { type: 'integer' },
description: '农场ID列表'
},
format: {
type: 'string',
enum: ['excel', 'pdf'],
description: '报表格式'
}
}
}
}
}
},
responses: {
200: { $ref: '#/components/responses/Success' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
},
'/api/reports/animal': {
post: {
summary: '生成动物统计报表',
tags: ['报表管理'],
security: [{ bearerAuth: [] }],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
startDate: { type: 'string', format: 'date', description: '开始日期' },
endDate: { type: 'string', format: 'date', description: '结束日期' },
farmId: { type: 'integer', description: '农场ID' },
format: {
type: 'string',
enum: ['excel', 'pdf'],
description: '报表格式'
}
}
}
}
}
},
responses: {
200: { $ref: '#/components/responses/Success' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
},
'/api/reports/device': {
post: {
summary: '生成设备统计报表',
tags: ['报表管理'],
security: [{ bearerAuth: [] }],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
startDate: { type: 'string', format: 'date', description: '开始日期' },
endDate: { type: 'string', format: 'date', description: '结束日期' },
format: {
type: 'string',
enum: ['excel', 'pdf'],
description: '报表格式'
}
}
}
}
}
},
responses: {
200: { $ref: '#/components/responses/Success' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
}
};
/**
* @swagger
* components:
* schemas:
* ReportGenerateRequest:
* type: object
* properties:
* startDate:
* type: string
* format: date
* description: 开始日期
* example: "2024-01-01"
* endDate:
* type: string
* format: date
* description: 结束日期
* example: "2024-01-31"
* format:
* type: string
* enum: [pdf, excel, csv]
* description: 报表格式
* example: "pdf"
*
* FarmReportRequest:
* allOf:
* - $ref: '#/components/schemas/ReportGenerateRequest'
* - type: object
* properties:
* farmIds:
* type: array
* items:
* type: string
* description: 养殖场ID列表
* example: ["farm_001", "farm_002"]
*
* SalesReportRequest:
* allOf:
* - $ref: '#/components/schemas/ReportGenerateRequest'
* - type: object
* properties:
* format:
* type: string
* enum: [pdf, excel]
* description: 报表格式销售报表不支持CSV
* example: "excel"
*
* ComplianceReportRequest:
* allOf:
* - $ref: '#/components/schemas/ReportGenerateRequest'
* - type: object
* properties:
* format:
* type: string
* enum: [pdf, excel]
* description: 报表格式合规报表不支持CSV
* example: "pdf"
*
* ReportFile:
* type: object
* properties:
* fileName:
* type: string
* description: 文件名
* example: "farm_report_20240115.pdf"
* downloadUrl:
* type: string
* description: 下载链接
* example: "/api/reports/download/farm_report_20240115.pdf"
* mimeType:
* type: string
* description: 文件MIME类型
* example: "application/pdf"
* size:
* type: integer
* description: 文件大小(字节)
* example: 1024000
* generatedAt:
* type: string
* format: date-time
* description: 生成时间
* example: "2024-01-15T10:30:00Z"
*
* ReportListItem:
* type: object
* properties:
* id:
* type: string
* description: 报表ID
* example: "report_001"
* fileName:
* type: string
* description: 文件名
* example: "farm_report_20240115.pdf"
* type:
* type: string
* enum: [farm, sales, compliance, export]
* description: 报表类型
* example: "farm"
* format:
* type: string
* enum: [pdf, excel, csv]
* description: 文件格式
* example: "pdf"
* size:
* type: integer
* description: 文件大小(字节)
* example: 1024000
* status:
* type: string
* enum: [generating, completed, failed, expired]
* description: 报表状态
* example: "completed"
* createdBy:
* type: string
* description: 创建者
* example: "admin"
* createdAt:
* type: string
* format: date-time
* description: 创建时间
* example: "2024-01-15T10:30:00Z"
* expiresAt:
* type: string
* format: date-time
* description: 过期时间
* example: "2024-01-22T10:30:00Z"
* downloadUrl:
* type: string
* description: 下载链接
* example: "/api/reports/download/farm_report_20240115.pdf"
*
* ReportTemplate:
* type: object
* properties:
* id:
* type: string
* description: 模板ID
* example: "template_001"
* name:
* type: string
* description: 模板名称
* example: "养殖场月度报表模板"
* type:
* type: string
* enum: [farm, sales, compliance]
* description: 模板类型
* example: "farm"
* description:
* type: string
* description: 模板描述
* example: "包含养殖场基本信息、动物统计、设备状态等"
* fields:
* type: array
* items:
* type: object
* properties:
* name:
* type: string
* description: 字段名称
* label:
* type: string
* description: 字段标签
* type:
* type: string
* description: 字段类型
* required:
* type: boolean
* description: 是否必填
* description: 模板字段配置
* isDefault:
* type: boolean
* description: 是否为默认模板
* example: true
* createdAt:
* type: string
* format: date-time
* description: 创建时间
* example: "2024-01-15T10:30:00Z"
*
* ExportDataRequest:
* type: object
* properties:
* format:
* type: string
* enum: [excel, csv]
* description: 导出格式
* example: "excel"
* filters:
* type: object
* description: 筛选条件
* properties:
* status:
* type: string
* description: 状态筛选
* startDate:
* type: string
* format: date
* description: 开始日期
* endDate:
* type: string
* format: date
* description: 结束日期
*/
// 报表管理相关的数据模型定义
const reportsSchemas = {
ReportRequest: {
type: 'object',
properties: {
startDate: { type: 'string', format: 'date', description: '开始日期' },
endDate: { type: 'string', format: 'date', description: '结束日期' },
farmIds: {
type: 'array',
items: { type: 'integer' },
description: '农场ID列表'
},
format: {
type: 'string',
enum: ['excel', 'pdf'],
description: '报表格式'
}
}
},
ReportResponse: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
data: {
type: 'object',
properties: {
reportId: { type: 'string', description: '报表ID' },
downloadUrl: { type: 'string', description: '下载链接' },
fileName: { type: 'string', description: '文件名' }
}
}
}
}
};
/**
* @swagger
* /reports/farm:
* post:
* tags:
* - 报表管理
* summary: 生成养殖统计报表
* description: 生成指定时间范围和养殖场的统计报表
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/FarmReportRequest'
* responses:
* 200:
* description: 报表生成成功
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/ApiResponse'
* - type: object
* properties:
* data:
* $ref: '#/components/schemas/ReportFile'
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
module.exports = {
reportsPaths,
reportsSchemas
};
/**
* @swagger
* /reports/sales:
* post:
* tags:
* - 报表管理
* summary: 生成销售分析报表
* description: 生成指定时间范围的销售分析报表(需要管理员或经理权限)
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SalesReportRequest'
* responses:
* 200:
* description: 报表生成成功
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/ApiResponse'
* - type: object
* properties:
* data:
* $ref: '#/components/schemas/ReportFile'
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 403:
* description: 权限不足
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/compliance:
* post:
* tags:
* - 报表管理
* summary: 生成监管合规报表
* description: 生成指定时间范围的监管合规报表(仅限管理员)
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ComplianceReportRequest'
* responses:
* 200:
* description: 报表生成成功
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/ApiResponse'
* - type: object
* properties:
* data:
* $ref: '#/components/schemas/ReportFile'
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 403:
* description: 权限不足(仅限管理员)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/download/{fileName}:
* get:
* tags:
* - 报表管理
* summary: 下载报表文件
* description: 下载指定的报表文件
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: fileName
* required: true
* schema:
* type: string
* description: 文件名需要URL编码
* example: "farm_report_20240115.pdf"
* responses:
* 200:
* description: 文件下载成功
* content:
* application/pdf:
* schema:
* type: string
* format: binary
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
* text/csv:
* schema:
* type: string
* format: binary
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 403:
* description: 非法文件路径
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: 文件不存在
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/list:
* get:
* tags:
* - 报表管理
* summary: 获取报表列表
* description: 获取当前用户的报表列表,支持分页和筛选
* security:
* - bearerAuth: []
* parameters:
* - $ref: '#/components/parameters/PaginationQuery/properties/page'
* - $ref: '#/components/parameters/PaginationQuery/properties/limit'
* - in: query
* name: type
* schema:
* type: string
* enum: [farm, sales, compliance, export]
* description: 报表类型筛选
* - in: query
* name: status
* schema:
* type: string
* enum: [generating, completed, failed, expired]
* description: 报表状态筛选
* - in: query
* name: format
* schema:
* type: string
* enum: [pdf, excel, csv]
* description: 文件格式筛选
* - in: query
* name: startDate
* schema:
* type: string
* format: date
* description: 创建开始日期
* - in: query
* name: endDate
* schema:
* type: string
* format: date
* description: 创建结束日期
* responses:
* 200:
* description: 获取列表成功
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/ApiResponse'
* - type: object
* properties:
* data:
* type: object
* properties:
* reports:
* type: array
* items:
* $ref: '#/components/schemas/ReportListItem'
* pagination:
* type: object
* properties:
* total:
* type: integer
* example: 50
* page:
* type: integer
* example: 1
* limit:
* type: integer
* example: 10
* totalPages:
* type: integer
* example: 5
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/cleanup:
* post:
* tags:
* - 报表管理
* summary: 清理过期报表
* description: 清理过期的报表文件(仅限管理员)
* security:
* - bearerAuth: []
* requestBody:
* required: false
* content:
* application/json:
* schema:
* type: object
* properties:
* daysOld:
* type: integer
* description: 清理多少天前的文件
* example: 30
* force:
* type: boolean
* description: 是否强制清理(包括未过期的文件)
* example: false
* responses:
* 200:
* description: 清理成功
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/ApiResponse'
* - type: object
* properties:
* data:
* type: object
* properties:
* deletedCount:
* type: integer
* description: 删除的文件数量
* example: 15
* freedSpace:
* type: integer
* description: 释放的空间(字节)
* example: 15360000
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 403:
* description: 权限不足(仅限管理员)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/export/farms:
* get:
* tags:
* - 报表管理
* summary: 导出养殖场数据
* description: 导出养殖场基础数据为Excel或CSV格式
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: format
* schema:
* type: string
* enum: [excel, csv]
* default: excel
* description: 导出格式
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive, all]
* default: all
* description: 状态筛选
* responses:
* 200:
* description: 导出成功
* content:
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
* text/csv:
* schema:
* type: string
* format: binary
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/export/devices:
* get:
* tags:
* - 报表管理
* summary: 导出设备数据
* description: 导出设备基础数据为Excel或CSV格式
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: format
* schema:
* type: string
* enum: [excel, csv]
* default: excel
* description: 导出格式
* - in: query
* name: status
* schema:
* type: string
* enum: [online, offline, maintenance, all]
* default: all
* description: 设备状态筛选
* responses:
* 200:
* description: 导出成功
* content:
* application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
* schema:
* type: string
* format: binary
* text/csv:
* schema:
* type: string
* format: binary
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
/**
* @swagger
* /reports/templates:
* get:
* tags:
* - 报表管理
* summary: 获取报表模板列表
* description: 获取可用的报表模板列表
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: type
* schema:
* type: string
* enum: [farm, sales, compliance]
* description: 模板类型筛选
* responses:
* 200:
* description: 获取模板列表成功
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/ApiResponse'
* - type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/ReportTemplate'
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: 服务器内部错误
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
module.exports = {};

View File

@@ -1,520 +0,0 @@
/**
* 简化版Swagger配置
* @file swagger-simple.js
* @description 简化的Swagger配置确保API路径正确显示
*/
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '智能预警系统 API',
version: '1.0.0',
description: '智能耳标预警和智能项圈预警系统API文档',
contact: {
name: '开发团队',
email: 'dev@example.com'
}
},
servers: [
{
url: 'http://localhost:5350/api',
description: '开发环境'
}
],
tags: [
{
name: '智能耳标预警',
description: '智能耳标预警相关接口'
},
{
name: '智能项圈预警',
description: '智能项圈预警相关接口'
}
]
},
apis: ['./routes/smart-alerts.js']
};
const specs = swaggerJSDoc(options);
// 手动添加API路径确保它们出现在文档中
if (!specs.paths) {
specs.paths = {};
}
// 智能耳标预警API路径
specs.paths['/smart-alerts/public/eartag/stats'] = {
get: {
tags: ['智能耳标预警'],
summary: '获取智能耳标预警统计',
description: '获取智能耳标预警的统计数据,包括各类预警的数量和设备总数',
responses: {
'200': {
description: '获取统计成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/eartag'] = {
get: {
tags: ['智能耳标预警'],
summary: '获取智能耳标预警列表',
description: '获取智能耳标预警列表,支持分页、搜索和筛选',
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词'
},
{
name: 'alertType',
in: 'query',
schema: { type: 'string', enum: ['battery', 'offline', 'temperature', 'movement'] },
description: '预警类型筛选'
}
],
responses: {
'200': {
description: '获取列表成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'array', items: { type: 'object' } },
total: { type: 'integer' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/eartag/{id}'] = {
get: {
tags: ['智能耳标预警'],
summary: '获取单个智能耳标预警详情',
description: '获取指定ID的智能耳标预警详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
description: '预警ID'
}
],
responses: {
'200': {
description: '获取详情成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/eartag/{id}/handle'] = {
post: {
tags: ['智能耳标预警'],
summary: '处理智能耳标预警',
description: '处理指定的智能耳标预警',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
description: '预警ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
action: { type: 'string', description: '处理动作' },
notes: { type: 'string', description: '处理备注' },
handler: { type: 'string', description: '处理人' }
}
}
}
}
},
responses: {
'200': {
description: '处理成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/eartag/batch-handle'] = {
post: {
tags: ['智能耳标预警'],
summary: '批量处理智能耳标预警',
description: '批量处理多个智能耳标预警',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['alertIds'],
properties: {
alertIds: { type: 'array', items: { type: 'string' }, description: '预警ID列表' },
action: { type: 'string', description: '处理动作' },
notes: { type: 'string', description: '处理备注' },
handler: { type: 'string', description: '处理人' }
}
}
}
}
},
responses: {
'200': {
description: '批量处理成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/eartag/export'] = {
get: {
tags: ['智能耳标预警'],
summary: '导出智能耳标预警数据',
description: '导出智能耳标预警数据支持JSON和CSV格式',
parameters: [
{
name: 'format',
in: 'query',
schema: { type: 'string', enum: ['json', 'csv'], default: 'json' },
description: '导出格式'
}
],
responses: {
'200': {
description: '导出成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'array', items: { type: 'object' } },
message: { type: 'string' }
}
}
}
}
}
}
}
};
// 智能项圈预警API路径
specs.paths['/smart-alerts/public/collar/stats'] = {
get: {
tags: ['智能项圈预警'],
summary: '获取智能项圈预警统计',
description: '获取智能项圈预警的统计数据,包括各类预警的数量和设备总数',
responses: {
'200': {
description: '获取统计成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/collar'] = {
get: {
tags: ['智能项圈预警'],
summary: '获取智能项圈预警列表',
description: '获取智能项圈预警列表,支持分页、搜索和筛选',
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词'
},
{
name: 'alertType',
in: 'query',
schema: { type: 'string', enum: ['battery', 'offline', 'temperature', 'movement', 'wear'] },
description: '预警类型筛选'
}
],
responses: {
'200': {
description: '获取列表成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'array', items: { type: 'object' } },
total: { type: 'integer' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/collar/{id}'] = {
get: {
tags: ['智能项圈预警'],
summary: '获取单个智能项圈预警详情',
description: '获取指定ID的智能项圈预警详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
description: '预警ID'
}
],
responses: {
'200': {
description: '获取详情成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/collar/{id}/handle'] = {
post: {
tags: ['智能项圈预警'],
summary: '处理智能项圈预警',
description: '处理指定的智能项圈预警',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' },
description: '预警ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
action: { type: 'string', description: '处理动作' },
notes: { type: 'string', description: '处理备注' },
handler: { type: 'string', description: '处理人' }
}
}
}
}
},
responses: {
'200': {
description: '处理成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/collar/batch-handle'] = {
post: {
tags: ['智能项圈预警'],
summary: '批量处理智能项圈预警',
description: '批量处理多个智能项圈预警',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['alertIds'],
properties: {
alertIds: { type: 'array', items: { type: 'string' }, description: '预警ID列表' },
action: { type: 'string', description: '处理动作' },
notes: { type: 'string', description: '处理备注' },
handler: { type: 'string', description: '处理人' }
}
}
}
}
},
responses: {
'200': {
description: '批量处理成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' },
message: { type: 'string' }
}
}
}
}
}
}
}
};
specs.paths['/smart-alerts/public/collar/export'] = {
get: {
tags: ['智能项圈预警'],
summary: '导出智能项圈预警数据',
description: '导出智能项圈预警数据支持JSON和CSV格式',
parameters: [
{
name: 'format',
in: 'query',
schema: { type: 'string', enum: ['json', 'csv'], default: 'json' },
description: '导出格式'
}
],
responses: {
'200': {
description: '导出成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'array', items: { type: 'object' } },
message: { type: 'string' }
}
}
}
}
}
}
}
};
module.exports = specs;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,521 +1,138 @@
/**
* 用户管理模块 Swagger 文档
* @file swagger-users.js
* @description 用户管理相关的 Swagger API 文档定义
*/
// 用户管理相关的 API 路径定义
const usersPaths = {
// 获取所有用户
'/users': {
'/api/users': {
get: {
summary: '获取所有用户',
tags: ['用户管理'],
summary: '获取用户列表',
description: '分页获取系统中的所有用户',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'page',
in: 'query',
schema: { type: 'integer', default: 1 },
description: '页码'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '每页数量'
},
{
name: 'search',
in: 'query',
schema: { type: 'string' },
description: '搜索关键词(用户名、邮箱、手机号)'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['active', 'inactive', 'banned'] },
description: '用户状态筛选'
},
{
name: 'role',
in: 'query',
schema: { type: 'string' },
description: '角色筛选'
}
{ $ref: '#/components/parameters/PageParam' },
{ $ref: '#/components/parameters/LimitParam' },
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/User' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
}
}
}
200: { $ref: '#/components/responses/Success' },
401: { $ref: '#/components/responses/Unauthorized' }
}
},
post: {
tags: ['用户管理'],
summary: '创建新用户',
description: '管理员创建新用户账号',
tags: ['用户管理'],
security: [{ bearerAuth: [] }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['username', 'email', 'password'],
properties: {
username: { type: 'string', description: '用户名' },
email: { type: 'string', format: 'email', description: '邮箱' },
password: { type: 'string', minLength: 6, description: '密码' },
phone: { type: 'string', description: '手机号' },
realName: { type: 'string', description: '真实姓名' },
avatar: { type: 'string', description: '头像URL' },
status: { type: 'string', enum: ['active', 'inactive'], default: 'active' },
roleIds: { type: 'array', items: { type: 'integer' }, description: '角色ID列表' }
}
}
schema: { $ref: '#/components/schemas/UserInput' }
}
}
},
responses: {
'201': {
description: '创建成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '用户创建成功' },
data: { $ref: '#/components/schemas/User' }
}
}
}
}
},
'400': {
description: '请求参数错误或用户已存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
201: { $ref: '#/components/responses/Created' },
400: { $ref: '#/components/responses/BadRequest' }
}
}
},
// 根据用户名搜索用户
'/users/search': {
'/api/users/{id}': {
get: {
summary: '根据ID获取用户',
tags: ['用户管理'],
summary: '搜索用户',
description: '根据用户名、邮箱或手机号搜索用户',
parameters: [
{
name: 'q',
in: 'query',
required: true,
schema: { type: 'string' },
description: '搜索关键词'
},
{
name: 'limit',
in: 'query',
schema: { type: 'integer', default: 10 },
description: '返回结果数量限制'
}
],
security: [{ bearerAuth: [] }],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '搜索成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: { $ref: '#/components/schemas/User' }
}
}
}
}
}
}
}
}
},
// 获取指定用户详情
'/users/{id}': {
get: {
tags: ['用户管理'],
summary: '获取用户详情',
description: '根据用户ID获取用户详细信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '用户ID'
}
],
responses: {
'200': {
description: '获取成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: { $ref: '#/components/schemas/User' }
}
}
}
}
},
'404': {
description: '用户不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
put: {
tags: ['用户管理'],
summary: '更新用户信息',
description: '更新指定用户的信息',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '用户ID'
}
],
tags: ['用户管理'],
security: [{ bearerAuth: [] }],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
username: { type: 'string', description: '用户名' },
email: { type: 'string', format: 'email', description: '邮箱' },
phone: { type: 'string', description: '手机号' },
realName: { type: 'string', description: '真实姓名' },
avatar: { type: 'string', description: '头像URL' },
status: { type: 'string', enum: ['active', 'inactive', 'banned'] },
roleIds: { type: 'array', items: { type: 'integer' }, description: '角色ID列表' }
}
}
schema: { $ref: '#/components/schemas/UserInput' }
}
}
},
responses: {
'200': {
description: '更新成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '用户信息更新成功' },
data: { $ref: '#/components/schemas/User' }
}
}
}
}
},
'400': {
description: '请求参数错误',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
},
'404': {
description: '用户不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
},
delete: {
tags: ['用户管理'],
summary: '删除用户',
description: '删除指定用户(软删除)',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '用户ID'
}
],
responses: {
'200': {
description: '删除成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '用户删除成功' }
}
}
}
}
},
'404': {
description: '用户不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
}
}
},
// 重置用户密码
'/users/{id}/reset-password': {
post: {
tags: ['用户管理'],
summary: '重置用户密码',
description: '管理员重置指定用户的密码',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: '用户ID'
}
],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['newPassword'],
properties: {
newPassword: {
type: 'string',
minLength: 6,
description: '新密码'
}
}
}
}
}
},
security: [{ bearerAuth: [] }],
parameters: [{ $ref: '#/components/parameters/IdParam' }],
responses: {
'200': {
description: '密码重置成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '密码重置成功' }
}
}
}
}
},
'404': {
description: '用户不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ErrorResponse' }
}
}
}
200: { $ref: '#/components/responses/Success' },
404: { $ref: '#/components/responses/NotFound' }
}
}
},
// 批量操作用户
'/users/batch': {
post: {
tags: ['用户管理'],
summary: '批量操作用户',
description: '批量启用、禁用或删除用户',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['userIds', 'action'],
properties: {
userIds: {
type: 'array',
items: { type: 'integer' },
description: '用户ID列表'
},
action: {
type: 'string',
enum: ['activate', 'deactivate', 'ban', 'delete'],
description: '操作类型'
}
}
}
}
}
},
responses: {
'200': {
description: '批量操作成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: '批量操作完成' },
data: {
type: 'object',
properties: {
successCount: { type: 'integer', description: '成功处理的用户数量' },
failedCount: { type: 'integer', description: '处理失败的用户数量' },
errors: { type: 'array', items: { type: 'string' }, description: '错误信息列表' }
}
}
}
}
}
}
}
}
}
},
// 导出用户数据
'/users/export': {
'/api/users/search': {
get: {
summary: '搜索用户',
tags: ['用户管理'],
summary: '导出用户数据',
description: '导出用户数据为Excel文件',
security: [{ bearerAuth: [] }],
parameters: [
{
name: 'format',
in: 'query',
schema: { type: 'string', enum: ['xlsx', 'csv'], default: 'xlsx' },
description: '导出格式'
},
{
name: 'status',
in: 'query',
schema: { type: 'string', enum: ['active', 'inactive', 'banned'] },
description: '用户状态筛选'
},
{
name: 'role',
in: 'query',
schema: { type: 'string' },
description: '角色筛选'
}
{ $ref: '#/components/parameters/SearchParam' }
],
responses: {
'200': {
description: '导出成功',
content: {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
schema: {
type: 'string',
format: 'binary'
}
},
'text/csv': {
schema: {
type: 'string',
format: 'binary'
}
}
}
}
200: { $ref: '#/components/responses/Success' }
}
}
}
};
// 用户数据模型
const userSchemas = {
// 用户管理相关的数据模型定义
const usersSchemas = {
User: {
type: 'object',
properties: {
id: { type: 'integer', description: '用户ID' },
username: { type: 'string', description: '用户名' },
email: { type: 'string', format: 'email', description: '邮箱' },
phone: { type: 'string', description: '手机号' },
realName: { type: 'string', description: '真实姓名' },
email: { type: 'string', format: 'email', description: '邮箱地址' },
phone: { type: 'string', description: '手机号' },
avatar: { type: 'string', description: '头像URL' },
status: {
type: 'string',
enum: ['active', 'inactive', 'banned'],
description: '用户状态active-活跃inactive-未激活banned-已封禁'
enum: ['active', 'inactive', 'suspended'],
description: '用户状态'
},
roles: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
description: { type: 'string' }
}
},
description: '用户角色列表'
},
permissions: {
type: 'array',
items: { type: 'string' },
description: '用户权限列表'
},
lastLoginAt: { type: 'string', format: 'date-time', description: '最后登录时间' },
createdAt: { type: 'string', format: 'date-time', description: '创建时间' },
updatedAt: { type: 'string', format: 'date-time', description: '更新时间' }
}
},
UserInput: {
type: 'object',
required: ['username', 'email', 'password'],
properties: {
username: { type: 'string', description: '用户名' },
email: { type: 'string', format: 'email', description: '邮箱地址' },
password: { type: 'string', format: 'password', description: '密码' },
phone: { type: 'string', description: '手机号码' },
avatar: { type: 'string', description: '头像URL' },
status: {
type: 'string',
enum: ['active', 'inactive', 'suspended'],
description: '用户状态'
}
}
}
};
module.exports = { usersPaths, userSchemas };
module.exports = {
usersPaths,
usersSchemas
};

View File

@@ -1,273 +0,0 @@
/**
* 预警检测逻辑测试
* @file test-alert-detection-logic.js
* @description 测试智能项圈预警的自动检测逻辑
*/
// 模拟前端判断函数
function determineAlertType(record) {
const alerts = []
// 检查电量预警
if (record.battery !== undefined && record.battery !== null && record.battery < 20) {
alerts.push('battery')
}
// 检查脱落预警 (bandge_status为0)
if (record.bandge_status !== undefined && record.bandge_status !== null && record.bandge_status === 0) {
alerts.push('wear')
}
// 检查离线预警 (is_connect为0)
if (record.is_connect !== undefined && record.is_connect !== null && record.is_connect === 0) {
alerts.push('offline')
}
// 检查温度预警
if (record.temperature !== undefined && record.temperature !== null) {
if (record.temperature < 20) {
alerts.push('temperature_low')
} else if (record.temperature > 40) {
alerts.push('temperature_high')
}
}
// 检查运动异常预警 (steps - y_steps为0)
if (record.steps !== undefined && record.y_steps !== undefined &&
record.steps !== null && record.y_steps !== null) {
const movementDiff = record.steps - record.y_steps
if (movementDiff === 0) {
alerts.push('movement')
}
}
// 返回第一个预警类型如果没有预警则返回null
return alerts.length > 0 ? alerts[0] : null
}
// 获取预警类型文本
function getAlertTypeText(type) {
const typeMap = {
'battery': '低电量预警',
'offline': '离线预警',
'temperature_low': '温度过低预警',
'temperature_high': '温度过高预警',
'movement': '异常运动预警',
'wear': '佩戴异常预警'
}
return typeMap[type] || '未知预警'
}
// 测试数据
const testCases = [
{
name: '正常设备',
data: {
battery: 85,
temperature: 25,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: null
},
{
name: '低电量预警',
data: {
battery: 15,
temperature: 25,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'battery'
},
{
name: '离线预警',
data: {
battery: 85,
temperature: 25,
is_connect: 0,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'offline'
},
{
name: '佩戴异常预警',
data: {
battery: 85,
temperature: 25,
is_connect: 1,
bandge_status: 0,
steps: 1000,
y_steps: 500
},
expected: 'wear'
},
{
name: '温度过低预警',
data: {
battery: 85,
temperature: 15,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'temperature_low'
},
{
name: '温度过高预警',
data: {
battery: 85,
temperature: 45,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'temperature_high'
},
{
name: '异常运动预警',
data: {
battery: 85,
temperature: 25,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 1000
},
expected: 'movement'
},
{
name: '多重预警(低电量+离线)',
data: {
battery: 15,
temperature: 25,
is_connect: 0,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'battery' // 应该返回第一个预警
},
{
name: '边界值测试 - 电量20',
data: {
battery: 20,
temperature: 25,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: null // 20不算低电量
},
{
name: '边界值测试 - 电量19',
data: {
battery: 19,
temperature: 25,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'battery' // 19算低电量
},
{
name: '边界值测试 - 温度20',
data: {
battery: 85,
temperature: 20,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: null // 20不算温度过低
},
{
name: '边界值测试 - 温度19',
data: {
battery: 85,
temperature: 19,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'temperature_low' // 19算温度过低
},
{
name: '边界值测试 - 温度40',
data: {
battery: 85,
temperature: 40,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: null // 40不算温度过高
},
{
name: '边界值测试 - 温度41',
data: {
battery: 85,
temperature: 41,
is_connect: 1,
bandge_status: 1,
steps: 1000,
y_steps: 500
},
expected: 'temperature_high' // 41算温度过高
}
];
// 运行测试
function runTests() {
console.log('🧪 开始测试预警检测逻辑...\n');
let passed = 0;
let failed = 0;
testCases.forEach((testCase, index) => {
const result = determineAlertType(testCase.data);
const expected = testCase.expected;
const success = result === expected;
console.log(`测试 ${index + 1}: ${testCase.name}`);
console.log(` 输入数据:`, testCase.data);
console.log(` 预期结果: ${expected ? getAlertTypeText(expected) : '正常'}`);
console.log(` 实际结果: ${result ? getAlertTypeText(result) : '正常'}`);
console.log(` 测试结果: ${success ? '✅ 通过' : '❌ 失败'}`);
console.log('');
if (success) {
passed++;
} else {
failed++;
}
});
console.log('📊 测试总结:');
console.log(` 总测试数: ${testCases.length}`);
console.log(` 通过: ${passed}`);
console.log(` 失败: ${failed}`);
console.log(` 成功率: ${((passed / testCases.length) * 100).toFixed(1)}%`);
if (failed === 0) {
console.log('\n🎉 所有测试通过!预警检测逻辑工作正常。');
} else {
console.log('\n⚠ 有测试失败,请检查预警检测逻辑。');
}
}
// 运行测试
runTests();

View File

@@ -1,229 +0,0 @@
/**
* 智能预警API综合测试脚本
* @file test-all-smart-alert-apis.js
* @description 测试智能耳标预警和智能项圈预警的所有API接口功能
*/
const eartagTests = require('./test-smart-eartag-alert-api');
const collarTests = require('./test-smart-collar-alert-api');
// 测试结果统计
let allTestResults = {
total: 0,
passed: 0,
failed: 0,
errors: []
};
// 测试辅助函数
function logTest(testName, success, message = '') {
allTestResults.total++;
if (success) {
allTestResults.passed++;
console.log(`${testName}: ${message}`);
} else {
allTestResults.failed++;
allTestResults.errors.push(`${testName}: ${message}`);
console.log(`${testName}: ${message}`);
}
}
// 综合测试函数
async function runComprehensiveTests() {
console.log('🚀 开始智能预警API综合测试...\n');
console.log('='.repeat(60));
console.log('📋 测试范围:');
console.log(' - 智能耳标预警API (6个接口)');
console.log(' - 智能项圈预警API (6个接口)');
console.log(' - 错误处理和边界条件测试');
console.log('='.repeat(60));
try {
// 测试智能耳标预警API
console.log('\n🔵 测试智能耳标预警API...');
console.log('-'.repeat(40));
const eartagResults = await runEartagTests();
logTest('智能耳标预警API测试', eartagResults.failed === 0,
`通过 ${eartagResults.passed}/${eartagResults.total} 项测试`);
// 测试智能项圈预警API
console.log('\n🟢 测试智能项圈预警API...');
console.log('-'.repeat(40));
const collarResults = await runCollarTests();
logTest('智能项圈预警API测试', collarResults.failed === 0,
`通过 ${collarResults.passed}/${collarResults.total} 项测试`);
// 测试API文档访问
console.log('\n📚 测试API文档访问...');
console.log('-'.repeat(40));
await testApiDocumentation();
// 输出综合测试结果
console.log('\n' + '='.repeat(60));
console.log('📊 综合测试结果汇总:');
console.log(`总测试数: ${allTestResults.total}`);
console.log(`通过: ${allTestResults.passed}`);
console.log(`失败: ${allTestResults.failed}`);
console.log(`成功率: ${((allTestResults.passed / allTestResults.total) * 100).toFixed(2)}%`);
if (allTestResults.errors.length > 0) {
console.log('\n❌ 失败详情:');
allTestResults.errors.forEach((error, index) => {
console.log(`${index + 1}. ${error}`);
});
}
if (allTestResults.failed === 0) {
console.log('\n🎉 所有测试通过智能预警API系统功能完全正常。');
console.log('\n📖 API文档访问地址: http://localhost:5350/api-docs');
console.log('🔗 基础API地址: http://localhost:5350/api/smart-alerts/public');
} else {
console.log('\n⚠ 部分测试失败,请检查相关功能。');
}
} catch (error) {
console.error('❌ 综合测试执行异常:', error.message);
}
}
// 运行智能耳标预警测试
async function runEartagTests() {
const results = { total: 0, passed: 0, failed: 0 };
try {
await eartagTests.testGetEartagAlertStats();
await eartagTests.testGetEartagAlerts();
await eartagTests.testGetEartagAlertsWithFilters();
await eartagTests.testGetEartagAlertById();
await eartagTests.testHandleEartagAlert();
await eartagTests.testBatchHandleEartagAlerts();
await eartagTests.testExportEartagAlerts();
await eartagTests.testErrorHandling();
// 这里需要从eartagTests模块获取结果但由于模块结构限制我们使用模拟数据
results.total = 8;
results.passed = 8; // 假设都通过
results.failed = 0;
} catch (error) {
console.error('智能耳标预警测试异常:', error.message);
results.failed++;
}
return results;
}
// 运行智能项圈预警测试
async function runCollarTests() {
const results = { total: 0, passed: 0, failed: 0 };
try {
await collarTests.testGetCollarAlertStats();
await collarTests.testGetCollarAlerts();
await collarTests.testGetCollarAlertsWithFilters();
await collarTests.testGetCollarAlertById();
await collarTests.testHandleCollarAlert();
await collarTests.testBatchHandleCollarAlerts();
await collarTests.testExportCollarAlerts();
await collarTests.testErrorHandling();
// 这里需要从collarTests模块获取结果但由于模块结构限制我们使用模拟数据
results.total = 8;
results.passed = 8; // 假设都通过
results.failed = 0;
} catch (error) {
console.error('智能项圈预警测试异常:', error.message);
results.failed++;
}
return results;
}
// 测试API文档访问
async function testApiDocumentation() {
try {
const axios = require('axios');
// 测试Swagger JSON文档
const swaggerResponse = await axios.get('http://localhost:5350/api-docs/swagger.json', {
timeout: 5000
});
if (swaggerResponse.status === 200) {
const swaggerSpec = swaggerResponse.data;
const hasEartagPaths = swaggerSpec.paths && Object.keys(swaggerSpec.paths).some(path => path.includes('/eartag'));
const hasCollarPaths = swaggerSpec.paths && Object.keys(swaggerSpec.paths).some(path => path.includes('/collar'));
logTest('Swagger JSON文档', true, '成功获取API文档规范');
logTest('耳标预警API文档', hasEartagPaths, hasEartagPaths ? '包含耳标预警API路径' : '缺少耳标预警API路径');
logTest('项圈预警API文档', hasCollarPaths, hasCollarPaths ? '包含项圈预警API路径' : '缺少项圈预警API路径');
} else {
logTest('Swagger JSON文档', false, `获取失败: HTTP ${swaggerResponse.status}`);
}
// 测试Swagger UI界面
const uiResponse = await axios.get('http://localhost:5350/api-docs/', {
timeout: 5000
});
if (uiResponse.status === 200) {
logTest('Swagger UI界面', true, '成功访问API文档界面');
} else {
logTest('Swagger UI界面', false, `访问失败: HTTP ${uiResponse.status}`);
}
} catch (error) {
logTest('API文档测试', false, `测试异常: ${error.message}`);
}
}
// 性能测试
async function runPerformanceTests() {
console.log('\n⚡ 性能测试...');
console.log('-'.repeat(40));
try {
const axios = require('axios');
const startTime = Date.now();
// 并发测试多个API
const promises = [
axios.get('http://localhost:5350/api/smart-alerts/public/eartag/stats'),
axios.get('http://localhost:5350/api/smart-alerts/public/collar/stats'),
axios.get('http://localhost:5350/api/smart-alerts/public/eartag?limit=5'),
axios.get('http://localhost:5350/api/smart-alerts/public/collar?limit=5')
];
const results = await Promise.all(promises);
const endTime = Date.now();
const duration = endTime - startTime;
const allSuccessful = results.every(response => response.status === 200);
logTest('并发API性能测试', allSuccessful, `4个API并发请求完成耗时 ${duration}ms`);
if (duration < 2000) {
logTest('响应时间测试', true, `响应时间良好: ${duration}ms`);
} else {
logTest('响应时间测试', false, `响应时间较慢: ${duration}ms`);
}
} catch (error) {
logTest('性能测试', false, `测试异常: ${error.message}`);
}
}
// 如果直接运行此脚本
if (require.main === module) {
runComprehensiveTests()
.then(() => runPerformanceTests())
.catch(console.error);
}
module.exports = {
runComprehensiveTests,
runPerformanceTests
};

View File

@@ -1,73 +0,0 @@
/**
* API访问测试脚本
* @file test-api-access.js
* @description 测试API接口是否正常访问
*/
const axios = require('axios');
async function testApiAccess() {
console.log('🔍 测试API访问...\n');
const baseUrl = 'http://localhost:5350';
try {
// 1. 测试服务器根路径
console.log('1. 测试服务器根路径...');
const rootResponse = await axios.get(`${baseUrl}/`);
console.log('✅ 服务器根路径正常:', rootResponse.data);
// 2. 测试API文档访问
console.log('\n2. 测试API文档访问...');
const docsResponse = await axios.get(`${baseUrl}/api-docs/`);
console.log('✅ API文档页面正常访问');
// 3. 测试Swagger JSON
console.log('\n3. 测试Swagger JSON...');
const swaggerResponse = await axios.get(`${baseUrl}/api-docs/swagger.json`);
console.log('✅ Swagger JSON正常:', swaggerResponse.data.info.title);
// 4. 测试智能耳标预警API
console.log('\n4. 测试智能耳标预警API...');
const eartagStatsResponse = await axios.get(`${baseUrl}/api/smart-alerts/public/eartag/stats`);
console.log('✅ 智能耳标预警统计API正常:', eartagStatsResponse.data.success);
// 5. 测试智能项圈预警API
console.log('\n5. 测试智能项圈预警API...');
const collarStatsResponse = await axios.get(`${baseUrl}/api/smart-alerts/public/collar/stats`);
console.log('✅ 智能项圈预警统计API正常:', collarStatsResponse.data.success);
// 6. 检查Swagger JSON中的路径
console.log('\n6. 检查Swagger JSON中的路径...');
const paths = Object.keys(swaggerResponse.data.paths || {});
const smartAlertPaths = paths.filter(path => path.includes('/smart-alerts/public'));
console.log('📋 找到的智能预警API路径:');
smartAlertPaths.forEach(path => {
console.log(` - ${path}`);
});
if (smartAlertPaths.length === 0) {
console.log('❌ 未找到智能预警API路径可能是Swagger配置问题');
} else {
console.log(`✅ 找到 ${smartAlertPaths.length} 个智能预警API路径`);
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.code === 'ECONNREFUSED') {
console.log('💡 建议: 请确保服务器已启动 (npm start)');
} else if (error.response) {
console.log('💡 建议: 检查API路径和端口配置');
console.log(' 状态码:', error.response.status);
console.log(' 响应:', error.response.data);
}
}
}
// 如果直接运行此脚本
if (require.main === module) {
testApiAccess().catch(console.error);
}
module.exports = { testApiAccess };

View File

@@ -1,88 +0,0 @@
/**
* 测试API响应
* @file test-api-response.js
* @description 测试智能项圈预警API是否返回正确的数据
*/
const axios = require('axios');
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
async function testApiResponse() {
console.log('🔍 测试智能项圈预警API响应...\n');
try {
// 1. 测试获取预警列表
console.log('1. 测试获取预警列表...');
const listResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
page: 1,
limit: 5
}
});
console.log('API响应状态:', listResponse.status);
console.log('API响应数据:', JSON.stringify(listResponse.data, null, 2));
if (listResponse.data.success) {
const data = listResponse.data.data || [];
console.log(`\n数据条数: ${data.length}`);
// 查找项圈22012000107的数据
const targetCollar = data.find(item => item.collarNumber == 22012000107);
if (targetCollar) {
console.log('\n找到项圈22012000107的数据:');
console.log('电量:', targetCollar.battery);
console.log('温度:', targetCollar.temperature);
console.log('预警类型:', targetCollar.alertType);
console.log('预警级别:', targetCollar.alertLevel);
console.log('完整数据:', JSON.stringify(targetCollar, null, 2));
} else {
console.log('\n未找到项圈22012000107的数据');
console.log('可用的项圈编号:');
data.forEach(item => {
console.log(`- ${item.collarNumber}`);
});
}
}
// 2. 测试搜索特定项圈
console.log('\n2. 测试搜索项圈22012000107...');
const searchResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
search: '22012000107',
page: 1,
limit: 10
}
});
if (searchResponse.data.success) {
const searchData = searchResponse.data.data || [];
console.log(`搜索到 ${searchData.length} 条数据`);
searchData.forEach((item, index) => {
console.log(`\n搜索结果${index + 1}:`);
console.log('项圈编号:', item.collarNumber);
console.log('电量:', item.battery);
console.log('温度:', item.temperature);
console.log('预警类型:', item.alertType);
console.log('预警级别:', item.alertLevel);
});
}
// 3. 测试统计数据
console.log('\n3. 测试统计数据...');
const statsResponse = await axios.get(`${BASE_URL}/collar/stats`);
console.log('统计数据:', JSON.stringify(statsResponse.data, null, 2));
} catch (error) {
console.error('❌ API测试失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
}
}
// 运行测试
testApiResponse().catch(console.error);

View File

@@ -1,111 +0,0 @@
/**
* 测试智能项圈预警数据
* @file test-collar-alert-data.js
* @description 测试智能项圈预警API返回的数据
*/
const axios = require('axios');
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
async function testCollarAlertData() {
console.log('🔍 测试智能项圈预警数据...\n');
try {
// 1. 测试获取预警列表
console.log('1. 获取预警列表数据...');
const listResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
page: 1,
limit: 10
}
});
console.log('API响应状态:', listResponse.status);
console.log('API响应数据:', JSON.stringify(listResponse.data, null, 2));
if (listResponse.data.success) {
const data = listResponse.data.data || [];
console.log(`\n数据条数: ${data.length}`);
if (data.length > 0) {
console.log('\n第一条数据示例:');
console.log(JSON.stringify(data[0], null, 2));
// 测试判断函数
console.log('\n测试预警判断逻辑:');
const testRecord = data[0];
const alertType = determineAlertType(testRecord);
console.log('判断结果:', alertType);
// 显示各字段值
console.log('\n字段值检查:');
console.log('battery:', testRecord.battery, typeof testRecord.battery);
console.log('temperature:', testRecord.temperature, typeof testRecord.temperature);
console.log('is_connect:', testRecord.is_connect, typeof testRecord.is_connect);
console.log('bandge_status:', testRecord.bandge_status, typeof testRecord.bandge_status);
console.log('steps:', testRecord.steps, typeof testRecord.steps);
console.log('y_steps:', testRecord.y_steps, typeof testRecord.y_steps);
} else {
console.log('⚠️ 没有数据返回');
}
} else {
console.log('❌ API调用失败:', listResponse.data.message);
}
// 2. 测试获取统计数据
console.log('\n2. 获取统计数据...');
const statsResponse = await axios.get(`${BASE_URL}/collar/stats`);
console.log('统计数据:', JSON.stringify(statsResponse.data, null, 2));
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
}
}
// 判断预警类型函数
function determineAlertType(record) {
const alerts = []
// 检查电量预警
if (record.battery !== undefined && record.battery !== null && record.battery < 20) {
alerts.push('battery')
}
// 检查脱落预警 (bandge_status为0)
if (record.bandge_status !== undefined && record.bandge_status !== null && record.bandge_status === 0) {
alerts.push('wear')
}
// 检查离线预警 (is_connect为0)
if (record.is_connect !== undefined && record.is_connect !== null && record.is_connect === 0) {
alerts.push('offline')
}
// 检查温度预警
if (record.temperature !== undefined && record.temperature !== null) {
if (record.temperature < 20) {
alerts.push('temperature_low')
} else if (record.temperature > 40) {
alerts.push('temperature_high')
}
}
// 检查运动异常预警 (steps - y_steps为0)
if (record.steps !== undefined && record.y_steps !== undefined &&
record.steps !== null && record.y_steps !== null) {
const movementDiff = record.steps - record.y_steps
if (movementDiff === 0) {
alerts.push('movement')
}
}
return alerts.length > 0 ? alerts[0] : null
}
// 运行测试
testCollarAlertData().catch(console.error);

View File

@@ -1,64 +0,0 @@
/**
* 直接测试API
* @file test-direct-api.js
* @description 直接测试智能项圈预警API不通过HTTP请求
*/
const { getCollarAlerts } = require('./controllers/smartCollarAlertController');
async function testDirectApi() {
console.log('🔍 直接测试智能项圈预警API...\n');
try {
// 模拟请求对象
const mockReq = {
query: {
page: 1,
limit: 5,
search: '22012000107'
}
};
// 模拟响应对象
const mockRes = {
json: (data) => {
console.log('API响应数据:');
console.log(JSON.stringify(data, null, 2));
if (data.success && data.data) {
const targetCollar = data.data.find(item => item.collarNumber == 22012000107);
if (targetCollar) {
console.log('\n找到项圈22012000107的数据:');
console.log('电量:', targetCollar.battery);
console.log('温度:', targetCollar.temperature);
console.log('预警类型:', targetCollar.alertType);
console.log('预警级别:', targetCollar.alertLevel);
} else {
console.log('\n未找到项圈22012000107的数据');
console.log('可用的项圈编号:');
data.data.forEach(item => {
console.log(`- ${item.collarNumber}`);
});
}
}
},
status: (code) => ({
json: (data) => {
console.log('错误响应:', code, data);
}
})
};
// 调用API函数
await getCollarAlerts(mockReq, mockRes);
} catch (error) {
console.error('❌ 测试失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行测试
testDirectApi().catch(console.error);

View File

@@ -1,91 +0,0 @@
/**
* 测试错误修复
* @file test-error-fix.js
* @description 测试修复后的智能项圈预警页面
*/
const axios = require('axios');
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
async function testErrorFix() {
console.log('🔧 测试错误修复...\n');
try {
// 获取预警列表数据
const response = await axios.get(`${BASE_URL}/collar`, {
params: { page: 1, limit: 3 }
});
if (response.data.success) {
const data = response.data.data || [];
const stats = response.data.stats || {};
console.log('✅ API调用成功');
console.log(`数据条数: ${data.length}`);
console.log('统计数据:', stats);
// 模拟前端数据转换逻辑
console.log('\n🔄 模拟前端数据转换...');
data.forEach((item, index) => {
console.log(`\n处理第${index + 1}条数据:`);
console.log('原始数据:', {
id: item.id,
alertType: item.alertType,
alertLevel: item.alertLevel,
collarNumber: item.collarNumber,
battery: item.battery,
temperature: item.temperature
});
// 模拟前端转换逻辑
let alertTypeText = '正常'
let alertLevel = 'low'
let determinedAlertType = null
if (item.alertType) {
const alertTypeMap = {
'battery': '低电量预警',
'offline': '离线预警',
'temperature': '温度预警',
'temperature_low': '温度过低预警',
'temperature_high': '温度过高预警',
'movement': '异常运动预警',
'wear': '佩戴异常预警'
}
alertTypeText = alertTypeMap[item.alertType] || item.alertType
determinedAlertType = item.alertType
const alertLevelMap = {
'high': '高级',
'medium': '中级',
'low': '低级',
'critical': '紧急'
}
alertLevel = alertLevelMap[item.alertLevel] || item.alertLevel
}
console.log('转换结果:', {
alertTypeText,
alertLevel,
determinedAlertType
});
});
console.log('\n✅ 数据转换测试通过没有ReferenceError');
} else {
console.log('❌ API调用失败:', response.data.message);
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
}
}
}
// 运行测试
testErrorFix().catch(console.error);

View File

@@ -1,81 +0,0 @@
/**
* 测试修复后的智能项圈预警
* @file test-fixed-collar-alert.js
* @description 测试修复后的智能项圈预警数据展示
*/
const axios = require('axios');
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
async function testFixedCollarAlert() {
console.log('🔧 测试修复后的智能项圈预警...\n');
try {
// 1. 获取预警列表
console.log('1. 获取预警列表...');
const listResponse = await axios.get(`${BASE_URL}/collar`, {
params: { page: 1, limit: 5 }
});
if (listResponse.data.success) {
const data = listResponse.data.data || [];
const stats = listResponse.data.stats || {};
console.log('✅ 数据获取成功');
console.log(`数据条数: ${data.length}`);
console.log('统计数据:', stats);
// 显示统计卡片数据
console.log('\n📊 统计卡片数据:');
console.log(`低电量预警: ${stats.lowBattery || 0}`);
console.log(`离线预警: ${stats.offline || 0}`);
console.log(`温度预警: ${stats.highTemperature || 0}`);
console.log(`异常运动预警: ${stats.abnormalMovement || 0}`);
console.log(`佩戴异常预警: ${stats.wearOff || 0}`);
// 显示前几条数据
console.log('\n📋 预警列表数据:');
data.slice(0, 3).forEach((item, index) => {
console.log(`\n${index + 1}条数据:`);
console.log(` 项圈编号: ${item.collarNumber}`);
console.log(` 预警类型: ${item.alertType}`);
console.log(` 预警级别: ${item.alertLevel}`);
console.log(` 设备电量: ${item.battery}%`);
console.log(` 设备温度: ${item.temperature}°C`);
console.log(` 当日步数: ${item.dailySteps}`);
});
} else {
console.log('❌ 数据获取失败:', listResponse.data.message);
}
// 2. 测试统计数据API
console.log('\n2. 测试统计数据API...');
const statsResponse = await axios.get(`${BASE_URL}/collar/stats`);
if (statsResponse.data.success) {
const statsData = statsResponse.data.data || {};
console.log('✅ 统计数据API正常');
console.log('统计数据:', statsData);
} else {
console.log('❌ 统计数据API失败:', statsResponse.data.message);
}
console.log('\n🎉 测试完成!');
console.log('\n💡 现在前端页面应该能正确显示:');
console.log(' - 统计卡片显示非零数据');
console.log(' - 预警列表显示正确的预警类型和级别');
console.log(' - 数据来自API而不是硬编码');
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error('响应状态:', error.response.status);
console.error('响应数据:', error.response.data);
}
}
}
// 运行测试
testFixedCollarAlert().catch(console.error);

View File

@@ -1,85 +0,0 @@
/**
* 测试模型连接
* @file test-model-connection.js
* @description 测试IotXqClient模型是否从正确的数据库读取数据
*/
const { IotXqClient } = require('./models');
async function testModelConnection() {
console.log('🔍 测试IotXqClient模型连接...\n');
try {
// 1. 测试数据库连接
console.log('1. 测试数据库连接...');
await IotXqClient.sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 2. 检查数据库配置
console.log('\n2. 检查数据库配置...');
const config = IotXqClient.sequelize.config;
console.log('数据库配置:');
console.log('主机:', config.host);
console.log('端口:', config.port);
console.log('数据库名:', config.database);
console.log('用户名:', config.username);
// 3. 查询项圈22012000107的数据
console.log('\n3. 查询项圈22012000107的数据...');
const devices = await IotXqClient.findAll({
where: {
sn: '22012000107'
},
order: [['uptime', 'DESC']]
});
console.log(`找到 ${devices.length} 条记录`);
devices.forEach((device, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', device.id);
console.log('SN:', device.sn);
console.log('设备ID:', device.deviceId);
console.log('电量:', device.battery, '(类型:', typeof device.battery, ')');
console.log('温度:', device.temperature, '(类型:', typeof device.temperature, ')');
console.log('状态:', device.state);
console.log('更新时间:', device.uptime);
});
// 4. 查询所有项圈的最新数据
console.log('\n4. 查询所有项圈的最新数据...');
const allDevices = await IotXqClient.findAll({
order: [['uptime', 'DESC']],
limit: 10
});
console.log('所有项圈的最新数据:');
allDevices.forEach((device, index) => {
console.log(`${index + 1}. SN: ${device.sn}, 电量: ${device.battery}, 温度: ${device.temperature}, 状态: ${device.state}`);
});
// 5. 检查是否有电量为99的记录
console.log('\n5. 检查是否有电量为99的记录...');
const battery99Devices = await IotXqClient.findAll({
where: {
battery: '99'
},
order: [['uptime', 'DESC']],
limit: 5
});
console.log(`找到 ${battery99Devices.length} 条电量为99的记录`);
battery99Devices.forEach((device, index) => {
console.log(`${index + 1}. SN: ${device.sn}, 电量: ${device.battery}, 温度: ${device.temperature}, 状态: ${device.state}`);
});
} catch (error) {
console.error('❌ 测试失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行测试
testModelConnection().catch(console.error);

View File

@@ -1,33 +0,0 @@
const { User, Role, Permission } = require('./models');
async function testModels() {
try {
console.log('测试模型关联...');
// 测试用户查询
const user = await User.findByPk(1, {
include: [{
model: Role,
as: 'role',
include: [{
model: Permission,
as: 'permissions',
through: { attributes: [] }
}]
}]
});
if (user) {
console.log('用户:', user.username);
console.log('角色:', user.role ? user.role.name : '无');
console.log('权限数量:', user.role && user.role.permissions ? user.role.permissions.length : 0);
} else {
console.log('未找到用户');
}
} catch (error) {
console.error('测试失败:', error.message);
}
}
testModels();

View File

@@ -1,49 +0,0 @@
@echo off
echo ========================================
echo ngrok测试脚本无需认证
echo ========================================
echo.
echo 注意此脚本使用ngrok的免费版本
echo 每次重启ngrokURL会发生变化
echo.
echo 选择要测试的服务:
echo 1. 后端服务 (端口5350)
echo 2. 前端服务 (端口5300)
echo 3. 同时启动两个服务
echo.
set /p choice="请输入选择 (1-3): "
if "%choice%"=="1" (
echo 启动后端服务穿透...
echo 请在新窗口中查看访问地址
start "ngrok-backend" .\ngrok.exe http 5350
) else if "%choice%"=="2" (
echo 启动前端服务穿透...
echo 请在新窗口中查看访问地址
start "ngrok-frontend" .\ngrok.exe http 5300
) else if "%choice%"=="3" (
echo 启动后端服务穿透...
start "ngrok-backend" .\ngrok.exe http 5350
timeout /t 2 /nobreak >nul
echo 启动前端服务穿透...
start "ngrok-frontend" .\ngrok.exe http 5300
) else (
echo 无效选择
goto end
)
echo.
echo ========================================
echo ngrok已启动
echo ========================================
echo.
echo 请查看新打开的窗口获取访问地址
echo 访问地址格式https://xxxxx.ngrok.io
echo.
:end
echo 按任意键退出...
pause >nul

View File

@@ -1,359 +0,0 @@
/**
* 智能项圈预警API测试脚本
* @file test-smart-collar-alert-api.js
* @description 测试智能项圈预警相关的API接口功能
*/
const axios = require('axios');
// 配置基础URL
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
// 创建axios实例
const api = axios.create({
baseURL: BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 测试结果统计
let testResults = {
total: 0,
passed: 0,
failed: 0,
errors: []
};
// 测试辅助函数
function logTest(testName, success, message = '') {
testResults.total++;
if (success) {
testResults.passed++;
console.log(`${testName}: ${message}`);
} else {
testResults.failed++;
testResults.errors.push(`${testName}: ${message}`);
console.log(`${testName}: ${message}`);
}
}
// 测试函数
async function testGetCollarAlertStats() {
try {
console.log('\n=== 测试获取智能项圈预警统计 ===');
const response = await api.get('/collar/stats');
if (response.status === 200 && response.data.success) {
const data = response.data.data;
logTest('获取项圈预警统计', true, `成功获取统计,设备总数: ${data.totalDevices}, 预警总数: ${data.totalAlerts}`);
// 验证数据结构
const requiredFields = ['totalDevices', 'lowBattery', 'offline', 'highTemperature', 'abnormalMovement', 'totalAlerts'];
const hasAllFields = requiredFields.every(field => data.hasOwnProperty(field));
logTest('统计数据结构验证', hasAllFields, hasAllFields ? '数据结构正确' : '缺少必要字段');
// 验证项圈特有字段
const hasWearOffField = data.hasOwnProperty('wearOff');
logTest('项圈特有字段验证', hasWearOffField, hasWearOffField ? '包含项圈脱落预警字段' : '缺少项圈脱落预警字段');
} else {
logTest('获取项圈预警统计', false, `请求失败: ${response.data.message || '未知错误'}`);
}
} catch (error) {
logTest('获取项圈预警统计', false, `请求异常: ${error.message}`);
}
}
async function testGetCollarAlerts() {
try {
console.log('\n=== 测试获取智能项圈预警列表 ===');
// 测试基础列表获取
const response = await api.get('/collar?page=1&limit=5');
if (response.status === 200 && response.data.success) {
const data = response.data;
logTest('获取项圈预警列表', true, `成功获取列表,共 ${data.total} 条预警`);
// 验证分页信息
const hasPagination = data.pagination && typeof data.pagination.page === 'number';
logTest('分页信息验证', hasPagination, hasPagination ? '分页信息正确' : '分页信息缺失');
// 验证统计数据
const hasStats = data.stats && typeof data.stats.lowBattery === 'number';
logTest('统计信息验证', hasStats, hasStats ? '统计信息正确' : '统计信息缺失');
// 验证项圈特有字段
if (data.data.length > 0) {
const firstAlert = data.data[0];
const hasCollarFields = firstAlert.hasOwnProperty('collarNumber') && firstAlert.hasOwnProperty('wearStatus');
logTest('项圈字段验证', hasCollarFields, hasCollarFields ? '包含项圈特有字段' : '缺少项圈特有字段');
}
} else {
logTest('获取项圈预警列表', false, `请求失败: ${response.data.message || '未知错误'}`);
}
} catch (error) {
logTest('获取项圈预警列表', false, `请求异常: ${error.message}`);
}
}
async function testGetCollarAlertsWithFilters() {
try {
console.log('\n=== 测试项圈预警列表筛选功能 ===');
// 测试按预警类型筛选
const batteryResponse = await api.get('/collar?alertType=battery&limit=3');
if (batteryResponse.status === 200 && batteryResponse.data.success) {
const batteryAlerts = batteryResponse.data.data;
const allBattery = batteryAlerts.every(alert => alert.alertType === 'battery');
logTest('按预警类型筛选', allBattery, `筛选结果: ${batteryAlerts.length} 条低电量预警`);
} else {
logTest('按预警类型筛选', false, `筛选失败: ${batteryResponse.data.message || '未知错误'}`);
}
// 测试项圈脱落预警筛选
const wearResponse = await api.get('/collar?alertType=wear&limit=3');
if (wearResponse.status === 200 && wearResponse.data.success) {
const wearAlerts = wearResponse.data.data;
const allWear = wearAlerts.every(alert => alert.alertType === 'wear');
logTest('项圈脱落预警筛选', allWear, `筛选结果: ${wearAlerts.length} 条项圈脱落预警`);
} else {
logTest('项圈脱落预警筛选', false, `筛选失败: ${wearResponse.data.message || '未知错误'}`);
}
// 测试搜索功能
const searchResponse = await api.get('/collar?search=COLLAR&limit=3');
if (searchResponse.status === 200 && searchResponse.data.success) {
const searchAlerts = searchResponse.data.data;
logTest('搜索功能', true, `搜索到 ${searchAlerts.length} 条相关预警`);
} else {
logTest('搜索功能', false, `搜索失败: ${searchResponse.data.message || '未知错误'}`);
}
} catch (error) {
logTest('筛选功能测试', false, `请求异常: ${error.message}`);
}
}
async function testGetCollarAlertById() {
try {
console.log('\n=== 测试获取单个项圈预警详情 ===');
// 首先获取一个预警ID
const listResponse = await api.get('/collar?limit=1');
if (listResponse.status === 200 && listResponse.data.success && listResponse.data.data.length > 0) {
const alertId = listResponse.data.data[0].id;
// 测试获取详情
const detailResponse = await api.get(`/collar/${alertId}`);
if (detailResponse.status === 200 && detailResponse.data.success) {
const alert = detailResponse.data.data;
logTest('获取项圈预警详情', true, `成功获取预警 ${alertId} 的详情`);
// 验证详情数据结构
const hasRequiredFields = alert.id && alert.alertType && alert.alertLevel;
logTest('详情数据结构验证', hasRequiredFields, hasRequiredFields ? '详情数据结构正确' : '缺少必要字段');
// 验证项圈特有字段
const hasCollarFields = alert.hasOwnProperty('collarNumber') && alert.hasOwnProperty('wearStatus');
logTest('项圈详情字段验证', hasCollarFields, hasCollarFields ? '包含项圈特有字段' : '缺少项圈特有字段');
} else {
logTest('获取项圈预警详情', false, `获取详情失败: ${detailResponse.data.message || '未知错误'}`);
}
} else {
logTest('获取项圈预警详情', false, '没有可用的预警数据用于测试');
}
} catch (error) {
logTest('获取项圈预警详情', false, `请求异常: ${error.message}`);
}
}
async function testHandleCollarAlert() {
try {
console.log('\n=== 测试处理项圈预警功能 ===');
// 首先获取一个预警ID
const listResponse = await api.get('/collar?limit=1');
if (listResponse.status === 200 && listResponse.data.success && listResponse.data.data.length > 0) {
const alertId = listResponse.data.data[0].id;
// 测试处理预警
const handleData = {
action: 'acknowledged',
notes: 'API测试处理项圈预警',
handler: 'test-user'
};
const handleResponse = await api.post(`/collar/${alertId}/handle`, handleData);
if (handleResponse.status === 200 && handleResponse.data.success) {
const result = handleResponse.data.data;
logTest('处理项圈预警', true, `成功处理预警 ${alertId}`);
// 验证处理结果
const hasProcessedInfo = result.alertId && result.action && result.processedAt;
logTest('处理结果验证', hasProcessedInfo, hasProcessedInfo ? '处理结果正确' : '处理结果不完整');
} else {
logTest('处理项圈预警', false, `处理失败: ${handleResponse.data.message || '未知错误'}`);
}
} else {
logTest('处理项圈预警', false, '没有可用的预警数据用于测试');
}
} catch (error) {
logTest('处理项圈预警', false, `请求异常: ${error.message}`);
}
}
async function testBatchHandleCollarAlerts() {
try {
console.log('\n=== 测试批量处理项圈预警功能 ===');
// 首先获取一些预警ID
const listResponse = await api.get('/collar?limit=3');
if (listResponse.status === 200 && listResponse.data.success && listResponse.data.data.length > 0) {
const alertIds = listResponse.data.data.map(alert => alert.id);
// 测试批量处理
const batchData = {
alertIds: alertIds,
action: 'acknowledged',
notes: 'API批量测试处理项圈预警',
handler: 'test-user'
};
const batchResponse = await api.post('/collar/batch-handle', batchData);
if (batchResponse.status === 200 && batchResponse.data.success) {
const result = batchResponse.data.data;
logTest('批量处理项圈预警', true, `成功批量处理 ${result.processedCount} 个预警`);
// 验证批量处理结果
const hasBatchResult = result.processedCount > 0 && Array.isArray(result.results);
logTest('批量处理结果验证', hasBatchResult, hasBatchResult ? '批量处理结果正确' : '批量处理结果不完整');
} else {
logTest('批量处理项圈预警', false, `批量处理失败: ${batchResponse.data.message || '未知错误'}`);
}
} else {
logTest('批量处理项圈预警', false, '没有足够的预警数据用于测试');
}
} catch (error) {
logTest('批量处理项圈预警', false, `请求异常: ${error.message}`);
}
}
async function testExportCollarAlerts() {
try {
console.log('\n=== 测试导出项圈预警数据功能 ===');
// 测试JSON格式导出
const exportResponse = await api.get('/collar/export?format=json&limit=5');
if (exportResponse.status === 200 && exportResponse.data.success) {
const exportData = exportResponse.data.data;
logTest('导出项圈预警数据', true, `成功导出 ${exportData.length} 条预警数据`);
// 验证导出数据
const hasExportData = Array.isArray(exportData) && exportData.length > 0;
logTest('导出数据验证', hasExportData, hasExportData ? '导出数据正确' : '导出数据为空');
// 验证项圈特有字段
if (exportData.length > 0) {
const firstAlert = exportData[0];
const hasCollarFields = firstAlert.hasOwnProperty('collarNumber') && firstAlert.hasOwnProperty('wearStatus');
logTest('导出数据项圈字段验证', hasCollarFields, hasCollarFields ? '导出数据包含项圈字段' : '导出数据缺少项圈字段');
}
} else {
logTest('导出项圈预警数据', false, `导出失败: ${exportResponse.data.message || '未知错误'}`);
}
} catch (error) {
logTest('导出项圈预警数据', false, `请求异常: ${error.message}`);
}
}
async function testErrorHandling() {
try {
console.log('\n=== 测试错误处理 ===');
// 测试无效的预警ID
const invalidIdResponse = await api.get('/collar/invalid_id');
if (invalidIdResponse.status === 400 || invalidIdResponse.status === 404) {
logTest('无效ID错误处理', true, '正确处理无效ID错误');
} else {
logTest('无效ID错误处理', false, '未正确处理无效ID错误');
}
// 测试无效的筛选参数
const invalidFilterResponse = await api.get('/collar?alertType=invalid_type');
if (invalidFilterResponse.status === 200) {
logTest('无效筛选参数处理', true, '正确处理无效筛选参数');
} else {
logTest('无效筛选参数处理', false, '未正确处理无效筛选参数');
}
} catch (error) {
logTest('错误处理测试', false, `请求异常: ${error.message}`);
}
}
// 主测试函数
async function runAllTests() {
console.log('🚀 开始智能项圈预警API测试...\n');
try {
await testGetCollarAlertStats();
await testGetCollarAlerts();
await testGetCollarAlertsWithFilters();
await testGetCollarAlertById();
await testHandleCollarAlert();
await testBatchHandleCollarAlerts();
await testExportCollarAlerts();
await testErrorHandling();
// 输出测试结果
console.log('\n' + '='.repeat(50));
console.log('📊 测试结果汇总:');
console.log(`总测试数: ${testResults.total}`);
console.log(`通过: ${testResults.passed}`);
console.log(`失败: ${testResults.failed}`);
console.log(`成功率: ${((testResults.passed / testResults.total) * 100).toFixed(2)}%`);
if (testResults.errors.length > 0) {
console.log('\n❌ 失败详情:');
testResults.errors.forEach((error, index) => {
console.log(`${index + 1}. ${error}`);
});
}
if (testResults.failed === 0) {
console.log('\n🎉 所有测试通过智能项圈预警API功能正常。');
} else {
console.log('\n⚠ 部分测试失败,请检查相关功能。');
}
} catch (error) {
console.error('❌ 测试执行异常:', error.message);
}
}
// 如果直接运行此脚本
if (require.main === module) {
runAllTests().catch(console.error);
}
module.exports = {
runAllTests,
testGetCollarAlertStats,
testGetCollarAlerts,
testGetCollarAlertById,
testHandleCollarAlert,
testBatchHandleCollarAlerts,
testExportCollarAlerts
};

View File

@@ -1,216 +0,0 @@
/**
* 智能项圈预警API集成测试
* @file test-smart-collar-alert-integration.js
* @description 测试智能项圈预警API的完整集成
*/
const axios = require('axios');
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
// 测试数据
const testData = {
collarNumber: 'TEST_COLLAR_001',
alertType: 'battery',
alertLevel: 'high',
battery: 15,
temperature: 25.5,
dailySteps: 1200,
longitude: 106.504962,
latitude: 26.547901
};
async function testCollarAlertAPI() {
console.log('🧪 开始测试智能项圈预警API集成...\n');
try {
// 1. 测试获取统计数据
console.log('1. 测试获取统计数据...');
const statsResponse = await axios.get(`${BASE_URL}/collar/stats`);
console.log('✅ 统计数据API正常');
console.log(' 响应:', statsResponse.data);
// 2. 测试获取预警列表
console.log('\n2. 测试获取预警列表...');
const listResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
page: 1,
limit: 10
}
});
console.log('✅ 预警列表API正常');
console.log(' 总数:', listResponse.data.total || 0);
console.log(' 数据条数:', listResponse.data.data ? listResponse.data.data.length : 0);
// 3. 测试搜索功能
console.log('\n3. 测试搜索功能...');
const searchResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
search: 'TEST',
page: 1,
limit: 10
}
});
console.log('✅ 搜索功能正常');
console.log(' 搜索结果数:', searchResponse.data.data ? searchResponse.data.data.length : 0);
// 4. 测试预警类型筛选
console.log('\n4. 测试预警类型筛选...');
const filterResponse = await axios.get(`${BASE_URL}/collar`, {
params: {
alertType: 'battery',
page: 1,
limit: 10
}
});
console.log('✅ 预警类型筛选正常');
console.log(' 筛选结果数:', filterResponse.data.data ? filterResponse.data.data.length : 0);
// 5. 测试获取单个预警详情
if (listResponse.data.data && listResponse.data.data.length > 0) {
const firstAlert = listResponse.data.data[0];
console.log('\n5. 测试获取单个预警详情...');
const detailResponse = await axios.get(`${BASE_URL}/collar/${firstAlert.id}`);
console.log('✅ 预警详情API正常');
console.log(' 预警ID:', firstAlert.id);
}
// 6. 测试处理预警
if (listResponse.data.data && listResponse.data.data.length > 0) {
const firstAlert = listResponse.data.data[0];
console.log('\n6. 测试处理预警...');
const handleResponse = await axios.post(`${BASE_URL}/collar/${firstAlert.id}/handle`, {
action: 'acknowledged',
notes: 'API测试处理',
handler: 'test_user'
});
console.log('✅ 处理预警API正常');
console.log(' 处理结果:', handleResponse.data);
}
// 7. 测试批量处理预警
if (listResponse.data.data && listResponse.data.data.length > 0) {
const alertIds = listResponse.data.data.slice(0, 2).map(alert => alert.id);
console.log('\n7. 测试批量处理预警...');
const batchHandleResponse = await axios.post(`${BASE_URL}/collar/batch-handle`, {
alertIds: alertIds,
action: 'acknowledged',
notes: 'API批量测试处理',
handler: 'test_user'
});
console.log('✅ 批量处理预警API正常');
console.log(' 批量处理结果:', batchHandleResponse.data);
}
// 8. 测试导出数据
console.log('\n8. 测试导出数据...');
const exportResponse = await axios.get(`${BASE_URL}/collar/export`, {
params: {
format: 'json'
}
});
console.log('✅ 导出数据API正常');
console.log(' 导出数据条数:', exportResponse.data.data ? exportResponse.data.data.length : 0);
console.log('\n🎉 所有API测试通过');
console.log('\n📋 API端点总结:');
console.log(' - GET /collar/stats - 获取统计数据');
console.log(' - GET /collar - 获取预警列表');
console.log(' - GET /collar/{id} - 获取预警详情');
console.log(' - POST /collar/{id}/handle - 处理预警');
console.log(' - POST /collar/batch-handle - 批量处理预警');
console.log(' - GET /collar/export - 导出数据');
} catch (error) {
console.error('❌ API测试失败:', error.message);
if (error.response) {
console.error(' 状态码:', error.response.status);
console.error(' 响应数据:', error.response.data);
}
if (error.code === 'ECONNREFUSED') {
console.log('\n💡 建议: 请确保后端服务器已启动');
console.log(' 启动命令: cd backend && npm start');
}
}
}
// 测试前端数据服务集成
async function testFrontendIntegration() {
console.log('\n🔍 测试前端数据服务集成...');
try {
// 模拟前端API调用
const frontendTests = [
{
name: '获取统计数据',
url: `${BASE_URL}/collar/stats`,
method: 'GET'
},
{
name: '获取预警列表',
url: `${BASE_URL}/collar`,
method: 'GET',
params: { page: 1, limit: 10 }
},
{
name: '搜索预警',
url: `${BASE_URL}/collar`,
method: 'GET',
params: { search: 'TEST', page: 1, limit: 10 }
},
{
name: '筛选预警',
url: `${BASE_URL}/collar`,
method: 'GET',
params: { alertType: 'battery', page: 1, limit: 10 }
}
];
for (const test of frontendTests) {
try {
const response = await axios({
method: test.method,
url: test.url,
params: test.params
});
console.log(`${test.name}: 成功`);
if (test.name === '获取统计数据') {
console.log(` 数据:`, response.data);
} else {
console.log(` 数据条数:`, response.data.data ? response.data.data.length : 0);
}
} catch (error) {
console.log(`${test.name}: 失败 - ${error.message}`);
}
}
} catch (error) {
console.error('❌ 前端集成测试失败:', error.message);
}
}
// 主函数
async function main() {
console.log('🚀 智能项圈预警API集成测试开始\n');
await testCollarAlertAPI();
await testFrontendIntegration();
console.log('\n✅ 测试完成!');
console.log('\n📖 前端页面应该能够:');
console.log(' 1. 动态显示统计数据(低电量、离线、温度、异常运动、佩戴异常)');
console.log(' 2. 显示预警列表数据');
console.log(' 3. 支持搜索和筛选功能');
console.log(' 4. 支持处理预警操作');
console.log(' 5. 支持导出数据功能');
}
// 如果直接运行此脚本
if (require.main === module) {
main().catch(console.error);
}
module.exports = { testCollarAlertAPI, testFrontendIntegration };

View File

@@ -1,326 +0,0 @@
/**
* 智能耳标预警API测试脚本
* @file test-smart-eartag-alert-api.js
* @description 测试智能耳标预警相关的API接口功能
*/
const axios = require('axios');
// 配置基础URL
const BASE_URL = 'http://localhost:5350/api/smart-alerts/public';
// 创建axios实例
const api = axios.create({
baseURL: BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 测试结果统计
let testResults = {
total: 0,
passed: 0,
failed: 0,
errors: []
};
// 测试辅助函数
function logTest(testName, success, message = '') {
testResults.total++;
if (success) {
testResults.passed++;
console.log(`${testName}: ${message}`);
} else {
testResults.failed++;
testResults.errors.push(`${testName}: ${message}`);
console.log(`${testName}: ${message}`);
}
}
// 测试函数
async function testGetEartagAlertStats() {
try {
console.log('\n=== 测试获取智能耳标预警统计 ===');
const response = await api.get('/eartag/stats');
if (response.status === 200 && response.data.success) {
const data = response.data.data;
logTest('获取预警统计', true, `成功获取统计,设备总数: ${data.totalDevices}, 预警总数: ${data.totalAlerts}`);
// 验证数据结构
const requiredFields = ['totalDevices', 'lowBattery', 'offline', 'highTemperature', 'abnormalMovement', 'totalAlerts'];
const hasAllFields = requiredFields.every(field => data.hasOwnProperty(field));
logTest('统计数据结构验证', hasAllFields, hasAllFields ? '数据结构正确' : '缺少必要字段');
} else {
logTest('获取预警统计', false, `请求失败: ${response.data.message || '未知错误'}`);
}
} catch (error) {
logTest('获取预警统计', false, `请求异常: ${error.message}`);
}
}
async function testGetEartagAlerts() {
try {
console.log('\n=== 测试获取智能耳标预警列表 ===');
// 测试基础列表获取
const response = await api.get('/eartag?page=1&limit=5');
if (response.status === 200 && response.data.success) {
const data = response.data;
logTest('获取预警列表', true, `成功获取列表,共 ${data.total} 条预警`);
// 验证分页信息
const hasPagination = data.pagination && typeof data.pagination.page === 'number';
logTest('分页信息验证', hasPagination, hasPagination ? '分页信息正确' : '分页信息缺失');
// 验证统计数据
const hasStats = data.stats && typeof data.stats.lowBattery === 'number';
logTest('统计信息验证', hasStats, hasStats ? '统计信息正确' : '统计信息缺失');
} else {
logTest('获取预警列表', false, `请求失败: ${response.data.message || '未知错误'}`);
}
} catch (error) {
logTest('获取预警列表', false, `请求异常: ${error.message}`);
}
}
async function testGetEartagAlertsWithFilters() {
try {
console.log('\n=== 测试预警列表筛选功能 ===');
// 测试按预警类型筛选
const batteryResponse = await api.get('/eartag?alertType=battery&limit=3');
if (batteryResponse.status === 200 && batteryResponse.data.success) {
const batteryAlerts = batteryResponse.data.data;
const allBattery = batteryAlerts.every(alert => alert.alertType === 'battery');
logTest('按预警类型筛选', allBattery, `筛选结果: ${batteryAlerts.length} 条低电量预警`);
} else {
logTest('按预警类型筛选', false, `筛选失败: ${batteryResponse.data.message || '未知错误'}`);
}
// 测试搜索功能
const searchResponse = await api.get('/eartag?search=EARTAG&limit=3');
if (searchResponse.status === 200 && searchResponse.data.success) {
const searchAlerts = searchResponse.data.data;
logTest('搜索功能', true, `搜索到 ${searchAlerts.length} 条相关预警`);
} else {
logTest('搜索功能', false, `搜索失败: ${searchResponse.data.message || '未知错误'}`);
}
} catch (error) {
logTest('筛选功能测试', false, `请求异常: ${error.message}`);
}
}
async function testGetEartagAlertById() {
try {
console.log('\n=== 测试获取单个预警详情 ===');
// 首先获取一个预警ID
const listResponse = await api.get('/eartag?limit=1');
if (listResponse.status === 200 && listResponse.data.success && listResponse.data.data.length > 0) {
const alertId = listResponse.data.data[0].id;
// 测试获取详情
const detailResponse = await api.get(`/eartag/${alertId}`);
if (detailResponse.status === 200 && detailResponse.data.success) {
const alert = detailResponse.data.data;
logTest('获取预警详情', true, `成功获取预警 ${alertId} 的详情`);
// 验证详情数据结构
const hasRequiredFields = alert.id && alert.alertType && alert.alertLevel;
logTest('详情数据结构验证', hasRequiredFields, hasRequiredFields ? '详情数据结构正确' : '缺少必要字段');
} else {
logTest('获取预警详情', false, `获取详情失败: ${detailResponse.data.message || '未知错误'}`);
}
} else {
logTest('获取预警详情', false, '没有可用的预警数据用于测试');
}
} catch (error) {
logTest('获取预警详情', false, `请求异常: ${error.message}`);
}
}
async function testHandleEartagAlert() {
try {
console.log('\n=== 测试处理预警功能 ===');
// 首先获取一个预警ID
const listResponse = await api.get('/eartag?limit=1');
if (listResponse.status === 200 && listResponse.data.success && listResponse.data.data.length > 0) {
const alertId = listResponse.data.data[0].id;
// 测试处理预警
const handleData = {
action: 'acknowledged',
notes: 'API测试处理',
handler: 'test-user'
};
const handleResponse = await api.post(`/eartag/${alertId}/handle`, handleData);
if (handleResponse.status === 200 && handleResponse.data.success) {
const result = handleResponse.data.data;
logTest('处理预警', true, `成功处理预警 ${alertId}`);
// 验证处理结果
const hasProcessedInfo = result.alertId && result.action && result.processedAt;
logTest('处理结果验证', hasProcessedInfo, hasProcessedInfo ? '处理结果正确' : '处理结果不完整');
} else {
logTest('处理预警', false, `处理失败: ${handleResponse.data.message || '未知错误'}`);
}
} else {
logTest('处理预警', false, '没有可用的预警数据用于测试');
}
} catch (error) {
logTest('处理预警', false, `请求异常: ${error.message}`);
}
}
async function testBatchHandleEartagAlerts() {
try {
console.log('\n=== 测试批量处理预警功能 ===');
// 首先获取一些预警ID
const listResponse = await api.get('/eartag?limit=3');
if (listResponse.status === 200 && listResponse.data.success && listResponse.data.data.length > 0) {
const alertIds = listResponse.data.data.map(alert => alert.id);
// 测试批量处理
const batchData = {
alertIds: alertIds,
action: 'acknowledged',
notes: 'API批量测试处理',
handler: 'test-user'
};
const batchResponse = await api.post('/eartag/batch-handle', batchData);
if (batchResponse.status === 200 && batchResponse.data.success) {
const result = batchResponse.data.data;
logTest('批量处理预警', true, `成功批量处理 ${result.processedCount} 个预警`);
// 验证批量处理结果
const hasBatchResult = result.processedCount > 0 && Array.isArray(result.results);
logTest('批量处理结果验证', hasBatchResult, hasBatchResult ? '批量处理结果正确' : '批量处理结果不完整');
} else {
logTest('批量处理预警', false, `批量处理失败: ${batchResponse.data.message || '未知错误'}`);
}
} else {
logTest('批量处理预警', false, '没有足够的预警数据用于测试');
}
} catch (error) {
logTest('批量处理预警', false, `请求异常: ${error.message}`);
}
}
async function testExportEartagAlerts() {
try {
console.log('\n=== 测试导出预警数据功能 ===');
// 测试JSON格式导出
const exportResponse = await api.get('/eartag/export?format=json&limit=5');
if (exportResponse.status === 200 && exportResponse.data.success) {
const exportData = exportResponse.data.data;
logTest('导出预警数据', true, `成功导出 ${exportData.length} 条预警数据`);
// 验证导出数据
const hasExportData = Array.isArray(exportData) && exportData.length > 0;
logTest('导出数据验证', hasExportData, hasExportData ? '导出数据正确' : '导出数据为空');
} else {
logTest('导出预警数据', false, `导出失败: ${exportResponse.data.message || '未知错误'}`);
}
} catch (error) {
logTest('导出预警数据', false, `请求异常: ${error.message}`);
}
}
async function testErrorHandling() {
try {
console.log('\n=== 测试错误处理 ===');
// 测试无效的预警ID
const invalidIdResponse = await api.get('/eartag/invalid_id');
if (invalidIdResponse.status === 400 || invalidIdResponse.status === 404) {
logTest('无效ID错误处理', true, '正确处理无效ID错误');
} else {
logTest('无效ID错误处理', false, '未正确处理无效ID错误');
}
// 测试无效的筛选参数
const invalidFilterResponse = await api.get('/eartag?alertType=invalid_type');
if (invalidFilterResponse.status === 200) {
logTest('无效筛选参数处理', true, '正确处理无效筛选参数');
} else {
logTest('无效筛选参数处理', false, '未正确处理无效筛选参数');
}
} catch (error) {
logTest('错误处理测试', false, `请求异常: ${error.message}`);
}
}
// 主测试函数
async function runAllTests() {
console.log('🚀 开始智能耳标预警API测试...\n');
try {
await testGetEartagAlertStats();
await testGetEartagAlerts();
await testGetEartagAlertsWithFilters();
await testGetEartagAlertById();
await testHandleEartagAlert();
await testBatchHandleEartagAlerts();
await testExportEartagAlerts();
await testErrorHandling();
// 输出测试结果
console.log('\n' + '='.repeat(50));
console.log('📊 测试结果汇总:');
console.log(`总测试数: ${testResults.total}`);
console.log(`通过: ${testResults.passed}`);
console.log(`失败: ${testResults.failed}`);
console.log(`成功率: ${((testResults.passed / testResults.total) * 100).toFixed(2)}%`);
if (testResults.errors.length > 0) {
console.log('\n❌ 失败详情:');
testResults.errors.forEach((error, index) => {
console.log(`${index + 1}. ${error}`);
});
}
if (testResults.failed === 0) {
console.log('\n🎉 所有测试通过智能耳标预警API功能正常。');
} else {
console.log('\n⚠ 部分测试失败,请检查相关功能。');
}
} catch (error) {
console.error('❌ 测试执行异常:', error.message);
}
}
// 如果直接运行此脚本
if (require.main === module) {
runAllTests().catch(console.error);
}
module.exports = {
runAllTests,
testGetEartagAlertStats,
testGetEartagAlerts,
testGetEartagAlertById,
testHandleEartagAlert,
testBatchHandleEartagAlerts,
testExportEartagAlerts
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,5 @@
const XLSX = require('xlsx');
const ExcelJS = require('exceljs');
const path = require('path');
const fs = require('fs');
@@ -73,6 +74,108 @@ class ExportUtils {
};
}
}
/**
* 导出数据到Excel文件带样式
* @param {Array} data - 要导出的数据数组
* @param {Array} columns - 列定义数组,包含 required 属性用于标识必填字段
* @param {String} filename - 文件名(不含扩展名)
* @returns {Object} 导出结果
*/
static async exportToExcelWithStyle(data, columns, filename = 'export') {
try {
// 使用ExcelJS创建工作簿
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sheet1');
// 设置表头
const headerRow = worksheet.addRow(columns.map(col => col.title));
// 设置表头样式(必填字段为红色,可选字段为黑色)
headerRow.eachCell((cell, colNumber) => {
const colIndex = colNumber - 1;
const col = columns[colIndex];
// 设置表头样式
cell.font = { bold: true, size: 11 };
cell.alignment = { vertical: 'middle', horizontal: 'center' };
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFFFFFFF' } // 白色背景
};
// 必填字段设置为红色字体
if (col.required) {
cell.font = { ...cell.font, color: { argb: 'FFFF0000' } }; // 红色
} else {
cell.font = { ...cell.font, color: { argb: 'FF000000' } }; // 黑色
}
// 设置边框
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
});
// 添加数据行
data.forEach(row => {
const rowData = columns.map(col => {
const value = row[col.dataIndex] || row[col.key] || '';
return value;
});
const dataRow = worksheet.addRow(rowData);
// 设置数据行样式
dataRow.eachCell((cell) => {
cell.alignment = { vertical: 'middle', horizontal: 'left' };
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' }
};
});
});
// 设置列宽
columns.forEach((col, index) => {
worksheet.getColumn(index + 1).width = col.width || 15;
});
// 生成文件路径
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `${filename}_${timestamp}.xlsx`;
const filePath = path.join(__dirname, '..', 'uploads', fileName);
// 确保uploads目录存在
const uploadsDir = path.dirname(filePath);
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// 写入文件
await workbook.xlsx.writeFile(filePath);
return {
success: true,
filePath: filePath,
fileName: fileName,
message: '导出成功'
};
} catch (error) {
console.error('导出Excel失败:', error);
return {
success: false,
message: error.message,
error: error
};
}
}
}
module.exports = ExportUtils;

View File

@@ -1,112 +0,0 @@
/**
* 验证数据库连接和数据
* @file verify-database-connection.js
* @description 重新验证数据库连接和项圈22012000107的数据
*/
const { sequelize } = require('./config/database-simple');
async function verifyDatabaseConnection() {
console.log('🔍 验证数据库连接和数据...\n');
try {
// 1. 测试数据库连接
console.log('1. 测试数据库连接...');
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 2. 直接查询项圈22012000107的数据
console.log('\n2. 直接查询项圈22012000107的数据...');
const [results] = await sequelize.query(`
SELECT
id, sn, deviceId, battery, temperature, state,
uptime, created_at, updated_at,
longitude, latitude, gps_state, rsrp
FROM iot_xq_client
WHERE sn = '22012000107'
ORDER BY uptime DESC
`);
console.log(`找到 ${results.length} 条记录`);
results.forEach((row, index) => {
console.log(`\n记录${index + 1}:`);
console.log('ID:', row.id);
console.log('SN:', row.sn);
console.log('设备ID:', row.deviceId);
console.log('电量:', row.battery);
console.log('温度:', row.temperature);
console.log('状态:', row.state);
console.log('经度:', row.longitude);
console.log('纬度:', row.latitude);
console.log('GPS状态:', row.gps_state);
console.log('RSRP:', row.rsrp);
console.log('更新时间:', row.uptime);
console.log('创建时间:', row.created_at);
console.log('更新时间:', row.updated_at);
});
// 3. 查询所有项圈的最新数据
console.log('\n3. 查询所有项圈的最新数据...');
const [allResults] = await sequelize.query(`
SELECT
id, sn, deviceId, battery, temperature, state, uptime
FROM iot_xq_client
ORDER BY uptime DESC
LIMIT 20
`);
console.log('所有项圈的最新数据:');
allResults.forEach((row, index) => {
console.log(`${index + 1}. SN: ${row.sn}, 电量: ${row.battery}, 温度: ${row.temperature}, 状态: ${row.state}, 更新时间: ${row.uptime}`);
});
// 4. 检查数据库配置
console.log('\n4. 检查数据库配置...');
const config = sequelize.config;
console.log('数据库配置:');
console.log('主机:', config.host);
console.log('端口:', config.port);
console.log('数据库名:', config.database);
console.log('用户名:', config.username);
// 5. 检查表结构
console.log('\n5. 检查表结构...');
const [tableInfo] = await sequelize.query(`
DESCRIBE iot_xq_client
`);
console.log('iot_xq_client表结构:');
tableInfo.forEach(col => {
console.log(`${col.Field}: ${col.Type} (${col.Null === 'YES' ? '可空' : '非空'})`);
});
// 6. 检查是否有多个数据库或表
console.log('\n6. 检查当前数据库...');
const [currentDb] = await sequelize.query('SELECT DATABASE() as current_db');
console.log('当前数据库:', currentDb[0].current_db);
// 7. 检查是否有其他包含项圈数据的表
console.log('\n7. 检查其他可能包含项圈数据的表...');
const [tables] = await sequelize.query(`
SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME LIKE '%collar%' OR TABLE_NAME LIKE '%xq%' OR TABLE_NAME LIKE '%iot%'
`);
console.log('相关表:');
tables.forEach(table => {
console.log('-', table.TABLE_NAME);
});
} catch (error) {
console.error('❌ 验证失败:', error.message);
console.error('错误详情:', error);
} finally {
process.exit(0);
}
}
// 运行验证
verifyDatabaseConnection().catch(console.error);

Some files were not shown because too many files have changed in this diff Show More