chore(project): 添加项目基础配置文件

添加了项目根目录的 .gitignore 文件,包含前端、后端、IDE等各类忽略规则;
新增 README.md 项目说明文档,详细介绍了 PawTrace 服务端的技术栈、目录结构、
模块功能、快速开始指南和版本规划等内容;
初始化 admin-pawtrace 管理端的基础文件,包括 package.json 依赖配置、
index.html 入口文件和 favicon.svg 图标文件。
```
This commit is contained in:
Yuhang Wu 2026-06-12 17:38:14 +08:00
parent 56c9f0b829
commit 12313b0c1e
121 changed files with 4636 additions and 0 deletions

138
.gitignore vendored Normal file
View File

@ -0,0 +1,138 @@
# ============================================================
# PawTrace 项目根 .gitignore
# 子模块: admin-pawtrace (Vite+TS) / uniapp-pawtrace (Uniapp)
# server-pawtrace (Spring Boot)
# ============================================================
# -------------------- 通用 --------------------
# 日志
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# 临时/缓存/备份
*.tmp
*.bak
*.swp
*.swo
*.orig
.cache/
.temp/
.tmp/
# 系统文件
.DS_Store
.DS_*
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# 环境/密钥(绝不入库)
.env
.env.*
!.env.example
*.pem
*.key
*.p12
*.keystore
credentials.json
secrets.yaml
# 测试覆盖率
coverage/
*.lcov
.nyc_output/
# -------------------- 前端通用 --------------------
# 依赖
node_modules/
jspm_packages/
bower_components/
# 构建产物
dist/
dist-ssr/
build/
out/
.next/
.nuxt/
.vite/
.parcel-cache/
.turbo/
.turbo-cache/
# TypeScript
*.tsbuildinfo
.tscache/
# -------------------- 管理端 admin-pawtrace (Vite) --------------------
.vite/
*.local
.vite-cache/
# -------------------- uniapp-pawtrace --------------------
# HBuilderX
.idea/
*.hbuilderx
HBuilderX.config
.project
unpackage/dist/
unpackage/release/
unpackage/cache/
unpackage/resources/
# 微信开发者工具产物
project.private.config.json
miniprogram_npm/
# -------------------- 服务端 server-pawtrace (Maven) --------------------
target/
build/
out/
*.class
*.jar
*.war
*.ear
hs_err_pid*
replay_pid*
# Maven Wrapper
.mvn/wrapper/maven-wrapper.jar
!/.mvn/wrapper/maven-wrapper.properties
# IDE - IntelliJ
.idea/
*.iml
*.iws
*.ipr
out/
# IDE - Eclipse / STS
.settings/
.classpath
.project
.factorypath
.springBeans
.sts4-cache/
bin/
# IDE - VSCode(保留 settings.json / 推荐插件)
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
# IDE - NetBeans
nbproject/
nbbuild/
nbdist/
# 上传文件(本地运行时生成)
upload/
uploads/
static/upload/

187
README.md Normal file
View File

@ -0,0 +1,187 @@
# 宠迹 PawTrace · 服务端
> 宠物成长记录与生活管理后端项目
> 配套小程序: [`uniapp-pawtrace`](../uniapp-pawtrace)
> 配套管理端: [`admin-pawtrace`](../admin-pawtrace)
## 一、项目简介
**宠迹(PawTrace)** 是一款面向个人宠物主的本地化、合规化宠物记录工具。服务端不参与任何用户间互动、不存储敏感人脸/位置信息,所有图片/视频处理均在客户端完成,服务端只负责元数据存取与提醒触达。
### 设计原则
- ✅ **本地优先**:服务端只做"陪伴",核心数据存本地,服务端兜底
- ✅ **合规底线**:无 UGC 公开、无评论点赞、无支付、无地图 UGC 标注
- ✅ **云端可选**:基础版可纯本地使用,登录后提供云端同步与多端访问
## 二、技术栈
| 类别 | 选型 |
|----------|--------------------------------------------|
| 基础框架 | Spring Boot 4.0.6 / Java 21 |
| 持久层 | MyBatis-Plus 3.5.15 + MySQL 8.x |
| 缓存 | Redis (Lettuce) |
| 鉴权 | Sa-Token 1.45.x |
| 工具集 | Hutool 5.8.x / Fastjson2 |
| 对象映射 | MapStruct 1.6.x |
| 微信生态 | WxJava 4.8.x (小程序 / 订阅消息) |
| 对象存储 | 七牛云 / 阿里云 OSS / 腾讯云 COS / MinIO / AWS S3 |
| 接口文档 | Knife4j 4.6.x (OpenAPI 3) |
| 构建 | Maven 3.9+ |
## 三、目录结构
```
server-pawtrace
├── pom.xml
├── README.md
└── src/main
├── java/com/pawtrace/server
│ ├── ServerPawtraceApplication.java # 启动类
│ ├── common
│ │ ├── constant/ # 常量 (CommonConstants / ResultCode)
│ │ ├── enums/ # 枚举 (宠物性别 / 日记类型 / 信息卡分类)
│ │ ├── exception/ # 全局异常 (BusinessException / GlobalExceptionHandler)
│ │ ├── page/ # 分页 (PageRequest / PageResult)
│ │ ├── result/ # 统一返回 Result
│ │ └── utils/ # 工具类
│ ├── config # 配置类 (MyBatis Plus / Redis / Sa-Token / 异步 / 跨域 / Knife4j)
│ ├── entity # 基础实体 (BaseEntity)
│ └── modules # 业务模块
│ ├── user # 用户与鉴权
│ ├── pet # 宠物档案 + 体重历史
│ ├── diary # 成长日记
│ ├── health # 健康医疗(疫苗/驱虫/就诊)
│ ├── inventory # 物资管家
│ ├── timemachine # 时光机工坊(仅元数据)
│ ├── lifecard # 宠物生活信息卡
│ ├── feedback # 意见反馈
│ ├── reminder # 提醒调度 (Scheduled)
│ └── file # 文件上传
└── resources
├── application.yml # 主配置
├── application-dev.yml # 开发环境
├── application-prod.yml # 生产环境
├── mapper/ # MyBatis XML(可空)
└── sql/pawtrace.sql # 初始化 SQL
```
## 四、模块与功能映射
| 模块 | 路径前缀 | 对应功能设计章节 | 核心接口 |
|-------------------||-------------|--------------------------------------------------------|
| 用户/鉴权 | `/auth` | 一、个人中心 | 微信登录 / 登出 |
| 宠物档案 | `/pet` | 一、宠物档案 | CRUD + 体重历史分页 |
| 成长日记 | `/diary` | 二、成长日记 | 时间轴分页(按年/月/类型/宠物筛选) |
| 健康医疗 | `/health` | 三、健康医疗本 | 疫苗 / 驱虫 / 就诊 CRUD + 提醒开关 |
| 物资管家 | `/inventory` | 四、物资管家 | CRUD + 低库存预警 + 消耗记录 |
| 时光机工坊 | `/timemachine` | 五、时光机工坊 | 保存作品元数据(实际处理在客户端) |
| 宠物生活信息卡 | `/life-card` | 六、生活信息卡 | 按分类查询 + 详情 |
| 意见反馈 | `/feedback` | 一、个人中心 | 提交反馈 |
| 提醒调度 | (内部 Scheduled) | 三、提醒功能 | 每天 08:00 扫描即将到期的疫苗/驱虫 |
| 文件上传 | `/file` | (通用) | 单文件上传,本地存储,生产可切换 OSS |
> **未实现功能(明确排除)**:用户间互动、私信、评论点赞、UGC 地图标注、电商/支付、AI 医疗诊断 —— 详见 [功能设计.md § 附录](../功能设计.md)。
## 五、快速开始
### 1. 环境准备
- JDK 21+
- Maven 3.9+
- MySQL 8.x
- Redis 7.x(可选,登录态需要)
### 2. 初始化数据库
```bash
mysql -u root -p < src/main/resources/sql/pawtrace.sql
```
### 3. 修改配置
编辑 `application-dev.yml`,修改 MySQL/Redis 账号密码及 `application.yml` 中的微信小程序 `appid/secret`、OSS 配置。
### 4. 启动
```bash
# 方式一:IDE 直接运行 ServerPawtraceApplication
# 方式二:命令行
mvn spring-boot:run
# 方式三:打包运行
mvn clean package -DskipTests
java -jar target/server-pawtrace-0.0.1.jar
```
启动成功后:
- 接口地址: <http://localhost:8080/api>
- 接口文档: <http://localhost:8080/api/doc.html>
## 六、核心约定
### 1. 统一返回
```json
{
"code": 200,
"message": "操作成功",
"data": { ... },
"timestamp": 1718000000000
}
```
业务码详见 [ResultCode.java](src/main/java/com/pawtrace/server/common/constant/ResultCode.java)。
### 2. 鉴权
使用 Sa-Token,前端在请求头携带 `Authorization: <token>`(由 `/auth/login` 返回)。除以下白名单外,所有接口需要登录:
```
/auth/login /auth/logout /file/**
/doc.html /swagger-ui.html /v3/api-docs/**
```
### 3. 业务约束
- 宠物上限:`MAX_PET_COUNT = 10`
- 单条日记图片上限:`MAX_DIARY_IMAGES = 9`
- 时光机视频图片上限:`MAX_TIMEMACHINE_IMAGES = 15`
- 提醒提前天数:支持 `1` / `3` / `7`
### 4. 数据隔离
所有业务表都带 `user_id` 字段,Service 层强制按当前登录用户过滤,严禁出现跨用户读取。
## 七、扩展指引
| 想要做的事 | 修改点 |
|------------------|----------------------------------------------------------------------------|
| 切换到 OSS | 实现 `OssService` 接口,替换 `FileController` 中的 `file.transferTo(...)` |
| 新增宠物种类 | 扩展 `Pet.species` 枚举值与 `PetGenderEnum` 等枚举 |
| 新增日记类型 | 扩展 `DiaryTypeEnum` |
| 接入实际的微信订阅消息推送 | `ReminderScheduledTask#scanUpcomingReminders``TODO` 位置,调用 `wx-java` 推送 |
| 接入管理端 | 现有接口已经支持按 userId 过滤,管理端只需另写一个 `ROLE_ADMIN` 路由 + 后台 `Controller` |
| 关闭/开启 WebSocket | `pom.xml` 已预留 `spring-boot-starter-websocket`,业务需要时编写 `WebSocket` Handler |
## 八、版本规划
- **v0.0.1(当前)**:项目骨架 + 基础 CRUD + 鉴权 + 提醒任务占位
- **v0.1.0**:微信登录接入、订阅消息推送、文件存储切换 OSS
- **v0.2.0**:管理端接口、统计接口、数据备份/恢复
- **v1.0.0**:多端同步、家庭共享(可选)
## 九、许可证
MIT License. 详见 [LICENSE](../LICENSE)(如有)。
## 十、贡献
1. Fork 本仓库
2. 在 `feature/xxx` 分支开发
3. 提交 PR 前请保证 `mvn clean compile` 通过
4. 提交信息请遵循 `feat: ...` / `fix: ...` / `docs: ...` 规范
---
> Made with ❤️ by PawTrace Team

13
admin-pawtrace/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin-pawtrace</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,38 @@
{
"name": "admin-pawtrace",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.34",
"axios": "^1.17.0",
"pinia": "^3.0.4",
"vue-router": "^5.1.0",
"ant-design-vue": "~4.2.6",
"@ant-design/icons-vue": "^7.0.1",
"@vueuse/core": "^14.3.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.2",
"echarts": "^6.1.0",
"nprogress": "^0.2.0",
"pinia-plugin-persistedstate": "^4.7.1"
},
"devDependencies": {
"@types/node": "^24.12.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
"typescript": "~6.0.2",
"vite": "^8.0.12",
"vue-tsc": "^3.2.8",
"@iconify/vue": "^5.0.1",
"sass": "^1.101.0",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0",
"vite-plugin-vue-devtools": "^8.1.2"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld />
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button type="button" class="counter" @click="count++">
Count is {{ count }}
</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>

View File

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

View File

@ -0,0 +1,296 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})

249
server-pawtrace/pom.xml Normal file
View File

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pawtrace</groupId>
<artifactId>server-pawtrace</artifactId>
<version>0.0.1</version>
<name>server-pawtrace</name>
<description>宠迹服务端 - 宠物成长记录与生活管理</description>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>4.0.6</spring-boot.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
<hutool.version>5.8.44</hutool.version>
<sa-token.version>1.45.0</sa-token.version>
<knife4j.version>4.6.0</knife4j.version>
<fastjson2.version>2.0.62</fastjson2.version>
<fastjson2-extension.version>2.0.61</fastjson2-extension.version>
<qiniu.version>7.19.0</qiniu.version>
<s3.version>2.44.4</s3.version>
<minio.version>9.0.0</minio.version>
<aliyun-oss.version>3.18.5</aliyun-oss.version>
<tencent-cos.version>5.6.269</tencent-cos.version>
<weixin-java.version>4.8.3.B</weixin-java.version>
<mapstruct.version>1.6.3</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot4-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring6</artifactId>
<version>${fastjson2-extension.version}</version>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>${qiniu.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${s3.version}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-oss.version}</version>
</dependency>
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>${tencent-cos.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/mapper/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.pawtrace.server.ServerPawtraceApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>huaweicloud</id>
<name>huawei</name>
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
</repository>
<repository>
<id>aliyunmaven</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>

View File

@ -0,0 +1,30 @@
package com.pawtrace.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 宠迹服务端启动类
*
* @author PawTrace Team
*/
@EnableAsync
@EnableScheduling
@SpringBootApplication
@MapperScan("com.pawtrace.server.mapper")
public class ServerPawtraceApplication {
public static void main(String[] args) {
SpringApplication.run(ServerPawtraceApplication.class, args);
System.out.println("""
====================================================
宠迹 PawTrace 服务端启动成功
Knife4j: http://localhost:8080/api/doc.html
====================================================
""");
}
}

View File

@ -0,0 +1,47 @@
package com.pawtrace.server.common.constant;
/**
* 通用常量
*
* @author PawTrace Team
*/
public final class CommonConstants {
private CommonConstants() {
}
/** 通用标识 */
public static final String DEFAULT = "default";
public static final String ROOT = "0";
public static final Long ROOT_ID = 0L;
/** 逻辑删除标识 */
public static final Integer DELETED = 1;
public static final Integer NOT_DELETED = 0;
/** 状态:启用/禁用 */
public static final Integer STATUS_ENABLED = 1;
public static final Integer STATUS_DISABLED = 0;
/** 是/否 */
public static final Integer YES = 1;
public static final Integer NO = 0;
/** 分页默认值 */
public static final Long DEFAULT_PAGE_NUM = 1L;
public static final Long DEFAULT_PAGE_SIZE = 10L;
public static final Long MAX_PAGE_SIZE = 200L;
/** 业务限制 */
public static final Integer MAX_PET_COUNT = 10;
public static final Integer MAX_DIARY_IMAGES = 9;
public static final Integer MAX_TIMEMACHINE_IMAGES = 15;
/** Header */
public static final String HEADER_AUTHORIZATION = "Authorization";
public static final String HEADER_USER_ID = "X-User-Id";
/** 角色标识 */
public static final String ROLE_USER = "user";
public static final String ROLE_ADMIN = "admin";
}

View File

@ -0,0 +1,47 @@
package com.pawtrace.server.common.constant;
import lombok.Getter;
/**
* 业务返回码枚举
*
* @author PawTrace Team
*/
@Getter
public enum ResultCode {
SUCCESS(200, "操作成功"),
FAIL(500, "操作失败"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未登录或登录已过期"),
FORBIDDEN(403, "无权访问"),
NOT_FOUND(404, "资源不存在"),
METHOD_NOT_ALLOWED(405, "请求方法不允许"),
// 业务相关 1xxx
USER_NOT_FOUND(1001, "用户不存在"),
USER_ALREADY_EXISTS(1002, "用户已存在"),
LOGIN_FAILED(1003, "登录失败"),
TOKEN_INVALID(1004, "令牌无效"),
PET_NOT_FOUND(2001, "宠物不存在"),
PET_LIMIT_EXCEEDED(2002, "宠物数量已达上限"),
DIARY_NOT_FOUND(3001, "日记不存在"),
HEALTH_RECORD_NOT_FOUND(4001, "健康记录不存在"),
INVENTORY_NOT_FOUND(5001, "物资不存在"),
FILE_UPLOAD_FAILED(9001, "文件上传失败"),
FILE_TYPE_INVALID(9002, "文件类型不支持");
private final Integer code;
private final String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}

View File

@ -0,0 +1,23 @@
package com.pawtrace.server.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 日记类型
*
* @author PawTrace Team
*/
@Getter
@AllArgsConstructor
public enum DiaryTypeEnum {
DAILY(1, "日常"),
GROWTH(2, "成长"),
MEDICAL(3, "医疗"),
DIET(4, "饮食"),
FUN(5, "趣事");
private final Integer code;
private final String desc;
}

View File

@ -0,0 +1,23 @@
package com.pawtrace.server.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 生活信息卡分类
*
* @author PawTrace Team
*/
@Getter
@AllArgsConstructor
public enum LifeCardCategoryEnum {
HOSPITAL(1, "宠物医院"),
RESTAURANT(2, "宠物友好餐厅"),
PARK(3, "宠物公园"),
STORE(4, "宠物店"),
GROOMING(5, "宠物美容");
private final Integer code;
private final String desc;
}

View File

@ -0,0 +1,21 @@
package com.pawtrace.server.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 宠物性别
*
* @author PawTrace Team
*/
@Getter
@AllArgsConstructor
public enum PetGenderEnum {
UNKNOWN(0, "未知"),
MALE(1, ""),
FEMALE(2, "");
private final Integer code;
private final String desc;
}

View File

@ -0,0 +1,41 @@
package com.pawtrace.server.common.exception;
import com.pawtrace.server.common.constant.ResultCode;
import lombok.Getter;
import java.io.Serial;
/**
* 业务异常
*
* @author PawTrace Team
*/
@Getter
public class BusinessException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/** 业务错误码 */
private final Integer code;
public BusinessException(String message) {
super(message);
this.code = ResultCode.FAIL.getCode();
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
}
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.code = resultCode.getCode();
}
}

View File

@ -0,0 +1,67 @@
package com.pawtrace.server.common.exception;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.common.constant.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
* @author PawTrace Team
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.warn("业务异常 [{}] {} -> {}", e.getCode(), request.getRequestURI(), e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.warn("参数校验失败: {}", msg);
return Result.fail(ResultCode.BAD_REQUEST.getCode(), msg);
}
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return Result.fail(ResultCode.BAD_REQUEST.getCode(), msg);
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public Result<Void> handleMissingParam(MissingServletRequestParameterException e) {
return Result.fail(ResultCode.BAD_REQUEST.getCode(), "缺少参数: " + e.getParameterName());
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Result<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(Result.fail(ResultCode.METHOD_NOT_ALLOWED.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
log.error("系统异常 [{}] {}", request.getRequestURI(), e.getMessage(), e);
return Result.fail(ResultCode.FAIL.getCode(), "系统繁忙,请稍后再试");
}
}

View File

@ -0,0 +1,31 @@
package com.pawtrace.server.common.page;
import com.pawtrace.server.common.constant.CommonConstants;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 分页请求
*
* @author PawTrace Team
*/
@Data
public class PageRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 当前页 */
private Long pageNum = CommonConstants.DEFAULT_PAGE_NUM;
/** 每页大小 */
private Long pageSize = CommonConstants.DEFAULT_PAGE_SIZE;
/** 排序字段 */
private String orderBy;
/** 排序方式: asc / desc */
private String order = "desc";
}

View File

@ -0,0 +1,52 @@
package com.pawtrace.server.common.page;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
/**
* 分页结果
*
* @param <T> 数据类型
* @author PawTrace Team
*/
@Data
public class PageResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 总记录数 */
private Long total;
/** 当前页 */
private Long pageNum;
/** 每页大小 */
private Long pageSize;
/** 总页数 */
private Long pages;
/** 数据列表 */
private List<T> records;
public PageResult() {
this.records = Collections.emptyList();
}
public PageResult(Long total, Long pageNum, Long pageSize, List<T> records) {
this.total = total;
this.pageNum = pageNum;
this.pageSize = pageSize;
this.pages = (total + pageSize - 1) / pageSize;
this.records = records;
}
public static <T> PageResult<T> empty(Long pageNum, Long pageSize) {
return new PageResult<>(0L, pageNum, pageSize, Collections.emptyList());
}
}

View File

@ -0,0 +1,74 @@
package com.pawtrace.server.common.result;
import com.pawtrace.server.common.constant.ResultCode;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 统一返回结果
*
* @param <T> 数据类型
*/
@Data
public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 业务状态码 */
private Integer code;
/** 提示信息 */
private String message;
/** 业务数据 */
private T data;
/** 时间戳 */
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success() {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
}
public static <T> Result<T> success(T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
}
public static <T> Result<T> fail() {
return new Result<>(ResultCode.FAIL.getCode(), ResultCode.FAIL.getMessage(), null);
}
public static <T> Result<T> fail(String message) {
return new Result<>(ResultCode.FAIL.getCode(), message, null);
}
public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(code, message, null);
}
public static <T> Result<T> fail(ResultCode resultCode) {
return new Result<>(resultCode.getCode(), resultCode.getMessage(), null);
}
public boolean isSuccess() {
return ResultCode.SUCCESS.getCode().equals(this.code);
}
}

View File

@ -0,0 +1,43 @@
package com.pawtrace.server.common.utils;
import com.pawtrace.server.common.constant.CommonConstants;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
/**
* 日期工具
*
* @author PawTrace Team
*/
public final class DateUtils {
private static final DateTimeFormatter DEFAULT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private DateUtils() {
}
/**
* 根据生日计算年龄(/)
*/
public static String calculateAge(LocalDate birthday) {
if (birthday == null) {
return "未知";
}
LocalDate now = LocalDate.now();
if (birthday.isAfter(now)) {
return "未出生";
}
Period period = Period.between(birthday, now);
if (period.getYears() <= 0) {
return period.getMonths() + "个月";
}
return period.getYears() + "" + (period.getMonths() > 0 ? period.getMonths() + "个月" : "");
}
public static String format(LocalDate date) {
return date == null ? CommonConstants.DEFAULT : date.format(DATE_FORMAT);
}
}

View File

@ -0,0 +1,32 @@
package com.pawtrace.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步任务 / 线程池配置
*
* @author PawTrace Team
*/
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("pawtraceTaskExecutor")
public Executor pawtraceTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("pawtrace-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,28 @@
package com.pawtrace.server.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Knife4j / OpenAPI 配置
*
* @author PawTrace Team
*/
@Configuration
public class Knife4jConfig {
@Bean
public OpenAPI pawtraceOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("宠迹 PawTrace 服务端 API")
.description("宠物成长记录与生活管理后端接口文档")
.version("v1.0.0")
.contact(new Contact().name("PawTrace Team").email("dev@pawtrace.com"))
.license(new License().name("MIT").url("https://opensource.org/licenses/MIT")));
}
}

View File

@ -0,0 +1,28 @@
package com.pawtrace.server.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis Plus 自动填充处理器
*
* @author PawTrace Team
*/
@Component
public class MybatisMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@ -0,0 +1,27 @@
package com.pawtrace.server.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis Plus 配置
*
* @author PawTrace Team
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

View File

@ -0,0 +1,43 @@
package com.pawtrace.server.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis 配置
*
* @author PawTrace Team
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
ObjectMapper om = new ObjectMapper();
om.registerModule(new JavaTimeModule());
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(om, Object.class);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

@ -0,0 +1,33 @@
package com.pawtrace.server.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Sa-Token 路由拦截配置
*
* @author PawTrace Team
*/
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/auth/login",
"/auth/logout",
"/doc.html",
"/swagger-ui.html",
"/swagger-resources/**",
"/webjars/**",
"/v3/api-docs/**",
"/favicon.ico",
"/file/**"
);
}
}

View File

@ -0,0 +1,24 @@
package com.pawtrace.server.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web 配置(CORS静态资源等)
*
* @author PawTrace Team
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@ -0,0 +1,41 @@
package com.pawtrace.server.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体
*
* @author PawTrace Team
*/
@Data
public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@Schema(description = "逻辑删除标识 0未删 1已删")
@TableLogic
@TableField("deleted")
private Integer deleted;
}

View File

@ -0,0 +1,50 @@
package com.pawtrace.server.modules.diary.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.page.PageRequest;
import com.pawtrace.server.common.page.PageResult;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.diary.entity.Diary;
import com.pawtrace.server.modules.diary.service.DiaryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
/**
* 成长日记 Controller
*
* @author PawTrace Team
*/
@Tag(name = "成长日记")
@RestController
@RequestMapping("/diary")
public class DiaryController {
private final DiaryService diaryService;
public DiaryController(DiaryService diaryService) {
this.diaryService = diaryService;
}
@Operation(summary = "时间轴分页")
@GetMapping("/page")
public Result<PageResult<Diary>> page(@RequestParam(required = false) Long petId,
@RequestParam(required = false) Integer diaryType,
@RequestParam(required = false) String yearMonth,
PageRequest request) {
return Result.success(diaryService.pageTimeline(StpUtil.getLoginIdAsLong(), petId, diaryType, yearMonth, request));
}
@Operation(summary = "新增/更新日记")
@PostMapping("/save")
public Result<Boolean> save(@RequestBody Diary diary) {
diary.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(diaryService.saveOrUpdate(diary));
}
@Operation(summary = "删除日记")
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(diaryService.removeById(id));
}
}

View File

@ -0,0 +1,52 @@
package com.pawtrace.server.modules.diary.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.time.LocalDate;
/**
* 成长日记
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_diary")
@Schema(description = "成长日记")
public class Diary extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "关联宠物ID")
private Long petId;
@Schema(description = "标题")
private String title;
@Schema(description = "内容")
private String content;
@Schema(description = "媒体URL列表(JSON数组字符串)")
private String mediaUrls;
@Schema(description = "心情标签")
private String moodTags;
@Schema(description = "地点文本")
private String location;
@Schema(description = "记录类型 1日常 2成长 3医疗 4饮食 5趣事")
private Integer diaryType;
@Schema(description = "发生日期")
private LocalDate happenDate;
}

View File

@ -0,0 +1,14 @@
package com.pawtrace.server.modules.diary.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.diary.entity.Diary;
import org.apache.ibatis.annotations.Mapper;
/**
* 日记 Mapper
*
* @author PawTrace Team
*/
@Mapper
public interface DiaryMapper extends BaseMapper<Diary> {
}

View File

@ -0,0 +1,19 @@
package com.pawtrace.server.modules.diary.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.common.page.PageRequest;
import com.pawtrace.server.common.page.PageResult;
import com.pawtrace.server.modules.diary.entity.Diary;
/**
* 日记 Service
*
* @author PawTrace Team
*/
public interface DiaryService extends IService<Diary> {
/**
* 时间轴分页(支持年份/月份/类型/宠物筛选)
*/
PageResult<Diary> pageTimeline(Long userId, Long petId, Integer diaryType, String yearMonth, PageRequest request);
}

View File

@ -0,0 +1,35 @@
package com.pawtrace.server.modules.diary.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.common.page.PageRequest;
import com.pawtrace.server.common.page.PageResult;
import com.pawtrace.server.modules.diary.entity.Diary;
import com.pawtrace.server.modules.diary.mapper.DiaryMapper;
import com.pawtrace.server.modules.diary.service.DiaryService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 日记 Service 实现
*
* @author PawTrace Team
*/
@Service
public class DiaryServiceImpl extends ServiceImpl<DiaryMapper, Diary> implements DiaryService {
@Override
public PageResult<Diary> pageTimeline(Long userId, Long petId, Integer diaryType, String yearMonth, PageRequest request) {
LambdaQueryWrapper<Diary> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Diary::getUserId, userId)
.eq(petId != null, Diary::getPetId, petId)
.eq(diaryType != null, Diary::getDiaryType, diaryType)
.likeRight(StringUtils.hasText(yearMonth), Diary::getHappenDate, yearMonth) // 形如 2025-06
.orderByDesc(Diary::getHappenDate, Diary::getCreateTime);
IPage<Diary> page = this.page(new Page<>(request.getPageNum(), request.getPageSize()), wrapper);
return new PageResult<>(page.getTotal(), request.getPageNum(), request.getPageSize(), page.getRecords());
}
}

View File

@ -0,0 +1,28 @@
package com.pawtrace.server.modules.feedback.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.feedback.entity.Feedback;
import com.pawtrace.server.modules.feedback.service.FeedbackService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
@Tag(name = "意见反馈")
@RestController
@RequestMapping("/feedback")
public class FeedbackController {
private final FeedbackService feedbackService;
public FeedbackController(FeedbackService feedbackService) {
this.feedbackService = feedbackService;
}
@Operation(summary = "提交反馈")
@PostMapping("/submit")
public Result<Boolean> submit(@RequestBody Feedback fb) {
fb.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(feedbackService.save(fb));
}
}

View File

@ -0,0 +1,39 @@
package com.pawtrace.server.modules.feedback.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 用户意见反馈
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_feedback")
@Schema(description = "意见反馈")
public class Feedback extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "反馈类型 1建议 2问题 3其他")
private Integer fbType;
@Schema(description = "内容")
private String content;
@Schema(description = "联系方式(可选)")
private String contact;
@Schema(description = "图片URL(JSON数组)")
private String images;
}

View File

@ -0,0 +1,8 @@
package com.pawtrace.server.modules.feedback.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.feedback.entity.Feedback;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FeedbackMapper extends BaseMapper<Feedback> {}

View File

@ -0,0 +1,6 @@
package com.pawtrace.server.modules.feedback.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.feedback.entity.Feedback;
public interface FeedbackService extends IService<Feedback> {}

View File

@ -0,0 +1,10 @@
package com.pawtrace.server.modules.feedback.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.modules.feedback.entity.Feedback;
import com.pawtrace.server.modules.feedback.mapper.FeedbackMapper;
import com.pawtrace.server.modules.feedback.service.FeedbackService;
import org.springframework.stereotype.Service;
@Service
public class FeedbackServiceImpl extends ServiceImpl<FeedbackMapper, Feedback> implements FeedbackService {}

View File

@ -0,0 +1,66 @@
package com.pawtrace.server.modules.file.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.pawtrace.server.common.constant.CommonConstants;
import com.pawtrace.server.common.constant.ResultCode;
import com.pawtrace.server.common.exception.BusinessException;
import com.pawtrace.server.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 文件上传 Controller
* <p>
* 当前实现为本地存储,生产环境可切换到 阿里云 OSS / 腾讯云 COS / 七牛云 / MinIO / S3
*
* @author PawTrace Team
*/
@Slf4j
@Tag(name = "文件上传")
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${oss.local.path:./upload/}")
private String localPath;
@Value("${oss.local.domain:http://localhost:8080/api/file/}")
private String domain;
@Operation(summary = "上传单个文件")
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new BusinessException(ResultCode.FILE_UPLOAD_FAILED);
}
String original = file.getOriginalFilename();
String ext = original != null && original.contains(".")
? original.substring(original.lastIndexOf('.'))
: "";
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String filename = IdUtil.simpleUUID() + ext;
File target = new File(localPath + dateDir + "/" + filename);
FileUtil.mkParentDirs(target);
file.transferTo(target);
String url = domain + dateDir + "/" + filename;
log.info("文件上传: {} -> {}", original, url);
return Result.success(url);
}
@Operation(summary = "静态资源回显(本地存储)")
@GetMapping("/**")
public void staticResource() {
// WebConfig 的静态资源映射处理
throw new BusinessException(ResultCode.NOT_FOUND);
}
}

View File

@ -0,0 +1,61 @@
package com.pawtrace.server.modules.health.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.health.entity.Vaccine;
import com.pawtrace.server.modules.health.entity.Deworm;
import com.pawtrace.server.modules.health.entity.VisitRecord;
import com.pawtrace.server.modules.health.service.HealthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
/**
* 健康医疗本 Controller
*
* @author PawTrace Team
*/
@Tag(name = "健康医疗")
@RestController
@RequestMapping("/health")
public class HealthController {
private final HealthService healthService;
public HealthController(HealthService healthService) {
this.healthService = healthService;
}
@Operation(summary = "新增/更新疫苗")
@PostMapping("/vaccine/save")
public Result<Boolean> saveVaccine(@RequestBody Vaccine v) {
v.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(healthService.vaccine().saveOrUpdate(v));
}
@Operation(summary = "删除疫苗")
@DeleteMapping("/vaccine/{id}")
public Result<Boolean> delVaccine(@PathVariable Long id) {
return Result.success(healthService.vaccine().removeById(id));
}
@Operation(summary = "新增/更新驱虫")
@PostMapping("/deworm/save")
public Result<Boolean> saveDeworm(@RequestBody Deworm d) {
d.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(healthService.deworm().saveOrUpdate(d));
}
@Operation(summary = "删除驱虫")
@DeleteMapping("/deworm/{id}")
public Result<Boolean> delDeworm(@PathVariable Long id) {
return Result.success(healthService.deworm().removeById(id));
}
@Operation(summary = "新增/更新就诊")
@PostMapping("/visit/save")
public Result<Boolean> saveVisit(@RequestBody VisitRecord v) {
v.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(healthService.visit().saveOrUpdate(v));
}
}

View File

@ -0,0 +1,55 @@
package com.pawtrace.server.modules.health.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.time.LocalDate;
/**
* 驱虫记录
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_deworm")
@Schema(description = "驱虫记录")
public class Deworm extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "宠物ID")
private Long petId;
@Schema(description = "驱虫药名称")
private String drugName;
@Schema(description = "类型 1体内 2体外")
private Integer drugType;
@Schema(description = "用药日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate useDate;
@Schema(description = "下次用药日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate nextDate;
@Schema(description = "包装照片")
private String drugImage;
@Schema(description = "提醒开关")
private Integer reminderEnabled;
@Schema(description = "提前提醒天数")
private Integer remindBeforeDays;
}

View File

@ -0,0 +1,55 @@
package com.pawtrace.server.modules.health.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.time.LocalDate;
/**
* 疫苗记录
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_vaccine")
@Schema(description = "疫苗记录")
public class Vaccine extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "宠物ID")
private Long petId;
@Schema(description = "疫苗名称")
private String vaccineName;
@Schema(description = "接种日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate injectDate;
@Schema(description = "下次接种日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate nextDate;
@Schema(description = "疫苗本照片URL")
private String certImage;
@Schema(description = "提醒 0关闭 1开启")
private Integer reminderEnabled;
@Schema(description = "提前提醒天数 1/3/7")
private Integer remindBeforeDays;
@Schema(description = "备注")
private String remark;
}

View File

@ -0,0 +1,49 @@
package com.pawtrace.server.modules.health.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 就诊记录
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_visit_record")
@Schema(description = "就诊记录")
public class VisitRecord extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "宠物ID")
private Long petId;
@Schema(description = "医院名称")
private String hospital;
@Schema(description = "就诊原因")
private String reason;
@Schema(description = "诊断结果")
private String diagnosis;
@Schema(description = "费用(可选)")
private BigDecimal cost;
@Schema(description = "就诊日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate visitDate;
}

View File

@ -0,0 +1,8 @@
package com.pawtrace.server.modules.health.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.health.entity.Deworm;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DewormMapper extends BaseMapper<Deworm> {}

View File

@ -0,0 +1,10 @@
package com.pawtrace.server.modules.health.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.health.entity.Vaccine;
import com.pawtrace.server.modules.health.entity.Deworm;
import com.pawtrace.server.modules.health.entity.VisitRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VaccineMapper extends BaseMapper<Vaccine> {}

View File

@ -0,0 +1,8 @@
package com.pawtrace.server.modules.health.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.health.entity.VisitRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VisitRecordMapper extends BaseMapper<VisitRecord> {}

View File

@ -0,0 +1,17 @@
package com.pawtrace.server.modules.health.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.health.entity.Vaccine;
import com.pawtrace.server.modules.health.entity.Deworm;
import com.pawtrace.server.modules.health.entity.VisitRecord;
/**
* 健康模块聚合 Service(疫苗/驱虫/就诊)
*
* @author PawTrace Team
*/
public interface HealthService {
IService<Vaccine> vaccine();
IService<Deworm> deworm();
IService<VisitRecord> visit();
}

View File

@ -0,0 +1,44 @@
package com.pawtrace.server.modules.health.service.impl;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.modules.health.entity.Vaccine;
import com.pawtrace.server.modules.health.entity.Deworm;
import com.pawtrace.server.modules.health.entity.VisitRecord;
import com.pawtrace.server.modules.health.mapper.VaccineMapper;
import com.pawtrace.server.modules.health.mapper.DewormMapper;
import com.pawtrace.server.modules.health.mapper.VisitRecordMapper;
import com.pawtrace.server.modules.health.service.HealthService;
import org.springframework.stereotype.Service;
/**
* 健康 Service 聚合(可由多个 Service 拆分,此处为统一入口)
*
* @author PawTrace Team
*/
@Service
public class HealthServiceImpl implements HealthService {
private final VaccineServiceImpl vaccineService;
private final DewormServiceImpl dewormService;
private final VisitRecordServiceImpl visitService;
public HealthServiceImpl(VaccineServiceImpl vaccineService,
DewormServiceImpl dewormService,
VisitRecordServiceImpl visitService) {
this.vaccineService = vaccineService;
this.dewormService = dewormService;
this.visitService = visitService;
}
@Override public IService<Vaccine> vaccine() { return vaccineService; }
@Override public IService<Deworm> deworm() { return dewormService; }
@Override public IService<VisitRecord> visit() { return visitService; }
@Service
public static class VaccineServiceImpl extends ServiceImpl<VaccineMapper, Vaccine> {}
@Service
public static class DewormServiceImpl extends ServiceImpl<DewormMapper, Deworm> {}
@Service
public static class VisitRecordServiceImpl extends ServiceImpl<VisitRecordMapper, VisitRecord> {}
}

View File

@ -0,0 +1,57 @@
package com.pawtrace.server.modules.inventory.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.inventory.entity.Inventory;
import com.pawtrace.server.modules.inventory.entity.InventoryLog;
import com.pawtrace.server.modules.inventory.service.InventoryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "物资管家")
@RestController
@RequestMapping("/inventory")
public class InventoryController {
private final InventoryService inventoryService;
public InventoryController(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Operation(summary = "我的物资列表")
@GetMapping("/list")
public Result<List<Inventory>> list() {
Long uid = StpUtil.getLoginIdAsLong();
return Result.success(inventoryService.lambdaQuery().eq(Inventory::getUserId, uid).list());
}
@Operation(summary = "新增/更新物资")
@PostMapping("/save")
public Result<Boolean> save(@RequestBody Inventory inv) {
inv.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(inventoryService.saveOrUpdate(inv));
}
@Operation(summary = "低库存预警列表")
@GetMapping("/low-stock")
public Result<List<Inventory>> lowStock() {
return Result.success(inventoryService.listLowStock(StpUtil.getLoginIdAsLong()));
}
@Operation(summary = "记录一次消耗")
@PostMapping("/consume")
public Result<Boolean> consume(@RequestBody InventoryLog log) {
log.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(inventoryService.consume(log));
}
@Operation(summary = "删除物资")
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(inventoryService.removeById(id));
}
}

View File

@ -0,0 +1,49 @@
package com.pawtrace.server.modules.inventory.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
/**
* 物资库存
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_inventory")
@Schema(description = "物资库存")
public class Inventory extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "分类 1粮 2罐头 3零食 4猫砂 5药品 6其他")
private Integer category;
@Schema(description = "名称")
private String name;
@Schema(description = "当前库存")
private BigDecimal stock;
@Schema(description = "单位 g/ml/袋/盒")
private String unit;
@Schema(description = "低库存阈值")
private BigDecimal lowStockThreshold;
@Schema(description = "封面图")
private String coverImage;
@Schema(description = "备注")
private String remark;
}

View File

@ -0,0 +1,37 @@
package com.pawtrace.server.modules.inventory.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
/**
* 物资消耗记录
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_inventory_log")
@Schema(description = "物资消耗记录")
public class InventoryLog extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "物资ID")
private Long inventoryId;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "消耗数量(正数)")
private BigDecimal quantity;
@Schema(description = "备注 如:吃了半袋")
private String remark;
}

View File

@ -0,0 +1,8 @@
package com.pawtrace.server.modules.inventory.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.inventory.entity.InventoryLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InventoryLogMapper extends BaseMapper<InventoryLog> {}

View File

@ -0,0 +1,9 @@
package com.pawtrace.server.modules.inventory.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.inventory.entity.Inventory;
import com.pawtrace.server.modules.inventory.entity.InventoryLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InventoryMapper extends BaseMapper<Inventory> {}

View File

@ -0,0 +1,16 @@
package com.pawtrace.server.modules.inventory.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.inventory.entity.Inventory;
import com.pawtrace.server.modules.inventory.entity.InventoryLog;
import java.util.List;
public interface InventoryService extends IService<Inventory> {
/** 当前用户的低库存物品 */
List<Inventory> listLowStock(Long userId);
/** 记录一次消耗,自动扣减库存 */
boolean consume(InventoryLog log);
}

View File

@ -0,0 +1,46 @@
package com.pawtrace.server.modules.inventory.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.common.exception.BusinessException;
import com.pawtrace.server.common.constant.ResultCode;
import com.pawtrace.server.modules.inventory.entity.Inventory;
import com.pawtrace.server.modules.inventory.entity.InventoryLog;
import com.pawtrace.server.modules.inventory.mapper.InventoryLogMapper;
import com.pawtrace.server.modules.inventory.mapper.InventoryMapper;
import com.pawtrace.server.modules.inventory.service.InventoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class InventoryServiceImpl extends ServiceImpl<InventoryMapper, Inventory> implements InventoryService {
private final InventoryLogMapper logMapper;
public InventoryServiceImpl(InventoryLogMapper logMapper) {
this.logMapper = logMapper;
}
@Override
public List<Inventory> listLowStock(Long userId) {
return this.list(new LambdaQueryWrapper<Inventory>()
.eq(Inventory::getUserId, userId)
.and(w -> w.apply("stock <= low_stock_threshold")));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean consume(InventoryLog log) {
Inventory item = this.getById(log.getInventoryId());
if (item == null) {
throw new BusinessException(ResultCode.INVENTORY_NOT_FOUND);
}
BigDecimal remain = item.getStock().subtract(log.getQuantity());
item.setStock(remain);
this.updateById(item);
return logMapper.insert(log) > 0;
}
}

View File

@ -0,0 +1,39 @@
package com.pawtrace.server.modules.lifecard.controller;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.lifecard.entity.LifeCard;
import com.pawtrace.server.modules.lifecard.service.LifeCardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 宠物生活信息卡 Controller
*
* @author PawTrace Team
*/
@Tag(name = "宠物生活信息卡")
@RestController
@RequestMapping("/life-card")
public class LifeCardController {
private final LifeCardService lifeCardService;
public LifeCardController(LifeCardService lifeCardService) {
this.lifeCardService = lifeCardService;
}
@Operation(summary = "按分类查询")
@GetMapping("/category/{category}")
public Result<List<LifeCard>> listByCategory(@PathVariable Integer category) {
return Result.success(lifeCardService.listByCategory(category));
}
@Operation(summary = "详情")
@GetMapping("/{id}")
public Result<LifeCard> detail(@PathVariable Long id) {
return Result.success(lifeCardService.getById(id));
}
}

View File

@ -0,0 +1,55 @@
package com.pawtrace.server.modules.lifecard.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
/**
* 宠物生活信息卡(商家/地点)
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_life_card")
@Schema(description = "生活信息卡")
public class LifeCard extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "分类 1医院 2餐厅 3公园 4宠物店 5美容")
private Integer category;
@Schema(description = "名称")
private String name;
@Schema(description = "封面图")
private String cover;
@Schema(description = "地址")
private String address;
@Schema(description = "联系电话")
private String phone;
@Schema(description = "简介")
private String description;
@Schema(description = "经度(腾讯地图GCJ02)")
private BigDecimal longitude;
@Schema(description = "纬度(腾讯地图GCJ02)")
private BigDecimal latitude;
@Schema(description = "排序")
private Integer sortOrder;
@Schema(description = "状态 0下架 1上架")
private Integer status;
}

View File

@ -0,0 +1,8 @@
package com.pawtrace.server.modules.lifecard.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.lifecard.entity.LifeCard;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LifeCardMapper extends BaseMapper<LifeCard> {}

View File

@ -0,0 +1,12 @@
package com.pawtrace.server.modules.lifecard.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.lifecard.entity.LifeCard;
import java.util.List;
public interface LifeCardService extends IService<LifeCard> {
/** 按分类查询上架的生活信息卡 */
List<LifeCard> listByCategory(Integer category);
}

View File

@ -0,0 +1,27 @@
package com.pawtrace.server.modules.lifecard.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.common.constant.CommonConstants;
import com.pawtrace.server.modules.lifecard.entity.LifeCard;
import com.pawtrace.server.modules.lifecard.mapper.LifeCardMapper;
import com.pawtrace.server.modules.lifecard.service.LifeCardService;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
public class LifeCardServiceImpl extends ServiceImpl<LifeCardMapper, LifeCard> implements LifeCardService {
@Override
public List<LifeCard> listByCategory(Integer category) {
if (category == null) {
return Collections.emptyList();
}
return this.list(new LambdaQueryWrapper<LifeCard>()
.eq(LifeCard::getCategory, category)
.eq(LifeCard::getStatus, CommonConstants.STATUS_ENABLED)
.orderByAsc(LifeCard::getSortOrder));
}
}

View File

@ -0,0 +1,47 @@
package com.pawtrace.server.modules.pet.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.pet.entity.Pet;
import com.pawtrace.server.modules.pet.service.PetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 宠物档案 Controller
*
* @author PawTrace Team
*/
@Tag(name = "宠物档案")
@RestController
@RequestMapping("/pet")
public class PetController {
private final PetService petService;
public PetController(PetService petService) {
this.petService = petService;
}
@Operation(summary = "我的宠物列表")
@GetMapping("/list")
public Result<List<Pet>> list() {
return Result.success(petService.listMyPets(StpUtil.getLoginIdAsLong()));
}
@Operation(summary = "新增/更新宠物")
@PostMapping("/save")
public Result<Boolean> save(@RequestBody Pet pet) {
pet.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(petService.saveOrUpdate(pet));
}
@Operation(summary = "删除宠物")
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(petService.removeById(id));
}
}

View File

@ -0,0 +1,55 @@
package com.pawtrace.server.modules.pet.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 宠物档案
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_pet")
@Schema(description = "宠物档案")
public class Pet extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "所属用户ID")
private Long userId;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "品种")
private String breed;
@Schema(description = "种类 1狗 2猫 3其他")
private Integer species;
@Schema(description = "性别 0未知 1公 2母")
private Integer gender;
@Schema(description = "生日")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate birthday;
@Schema(description = "当前体重(kg)")
private BigDecimal weight;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "性格标签(逗号分隔)")
private String personalityTags;
}

View File

@ -0,0 +1,43 @@
package com.pawtrace.server.modules.pet.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 体重历史记录
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_pet_weight")
@Schema(description = "体重记录")
public class PetWeight extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "宠物ID")
private Long petId;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "体重(kg)")
private BigDecimal weight;
@Schema(description = "记录日期")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate recordDate;
@Schema(description = "备注")
private String remark;
}

View File

@ -0,0 +1,15 @@
package com.pawtrace.server.modules.pet.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.pet.entity.Pet;
import com.pawtrace.server.modules.pet.entity.PetWeight;
import org.apache.ibatis.annotations.Mapper;
/**
* 宠物 Mapper
*
* @author PawTrace Team
*/
@Mapper
public interface PetMapper extends BaseMapper<Pet> {
}

View File

@ -0,0 +1,14 @@
package com.pawtrace.server.modules.pet.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.pet.entity.PetWeight;
import org.apache.ibatis.annotations.Mapper;
/**
* 体重记录 Mapper
*
* @author PawTrace Team
*/
@Mapper
public interface PetWeightMapper extends BaseMapper<PetWeight> {
}

View File

@ -0,0 +1,22 @@
package com.pawtrace.server.modules.pet.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.common.page.PageResult;
import com.pawtrace.server.modules.pet.entity.Pet;
import com.pawtrace.server.modules.pet.entity.PetWeight;
import java.util.List;
/**
* 宠物 Service
*
* @author PawTrace Team
*/
public interface PetService extends IService<Pet> {
/** 当前用户的宠物列表 */
List<Pet> listMyPets(Long userId);
/** 体重历史分页 */
PageResult<PetWeight> pageWeight(Long userId, Long petId, Long pageNum, Long pageSize);
}

View File

@ -0,0 +1,12 @@
package com.pawtrace.server.modules.pet.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.pet.entity.PetWeight;
/**
* 体重 Service
*
* @author PawTrace Team
*/
public interface PetWeightService extends IService<PetWeight> {
}

View File

@ -0,0 +1,45 @@
package com.pawtrace.server.modules.pet.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.common.page.PageResult;
import com.pawtrace.server.modules.pet.entity.Pet;
import com.pawtrace.server.modules.pet.entity.PetWeight;
import com.pawtrace.server.modules.pet.mapper.PetMapper;
import com.pawtrace.server.modules.pet.mapper.PetWeightMapper;
import com.pawtrace.server.modules.pet.service.PetService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 宠物 Service 实现
*
* @author PawTrace Team
*/
@Service
public class PetServiceImpl extends ServiceImpl<PetMapper, Pet> implements PetService {
private final PetWeightMapper petWeightMapper;
public PetServiceImpl(PetWeightMapper petWeightMapper) {
this.petWeightMapper = petWeightMapper;
}
@Override
public List<Pet> listMyPets(Long userId) {
return this.list(new LambdaQueryWrapper<Pet>().eq(Pet::getUserId, userId));
}
@Override
public PageResult<PetWeight> pageWeight(Long userId, Long petId, Long pageNum, Long pageSize) {
IPage<PetWeight> page = petWeightMapper.selectPage(new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<PetWeight>()
.eq(PetWeight::getUserId, userId)
.eq(petId != null, PetWeight::getPetId, petId)
.orderByDesc(PetWeight::getRecordDate));
return new PageResult<>(page.getTotal(), pageNum, pageSize, page.getRecords());
}
}

View File

@ -0,0 +1,16 @@
package com.pawtrace.server.modules.pet.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.modules.pet.entity.PetWeight;
import com.pawtrace.server.modules.pet.mapper.PetWeightMapper;
import com.pawtrace.server.modules.pet.service.PetWeightService;
import org.springframework.stereotype.Service;
/**
* 体重 Service 实现
*
* @author PawTrace Team
*/
@Service
public class PetWeightServiceImpl extends ServiceImpl<PetWeightMapper, PetWeight> implements PetWeightService {
}

View File

@ -0,0 +1,67 @@
package com.pawtrace.server.modules.reminder.scheduled;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pawtrace.server.modules.health.entity.Vaccine;
import com.pawtrace.server.modules.health.entity.Deworm;
import com.pawtrace.server.modules.health.mapper.VaccineMapper;
import com.pawtrace.server.modules.health.mapper.DewormMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
/**
* 提醒扫描任务
* <p>
* 每天 08:00 扫描即将到期的疫苗 / 驱虫,通过微信订阅消息推送给用户
* 实际推送实现依赖 wx-java miniapp,此处仅负责检索待推送列表
*
* @author PawTrace Team
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ReminderScheduledTask {
private final VaccineMapper vaccineMapper;
private final DewormMapper dewormMapper;
@Scheduled(cron = "0 0 8 * * ?")
public void scanUpcomingReminders() {
LocalDate today = LocalDate.now();
// 1) 即将到期(提前1天)
scanVaccine(today, 1);
scanVaccine(today, 3);
scanVaccine(today, 7);
scanDeworm(today, 1);
scanDeworm(today, 3);
scanDeworm(today, 7);
}
private void scanVaccine(LocalDate today, int beforeDays) {
LocalDate target = today.plusDays(beforeDays);
List<Vaccine> list = vaccineMapper.selectList(Wrappers.<Vaccine>lambdaQuery()
.eq(Vaccine::getReminderEnabled, 1)
.eq(Vaccine::getRemindBeforeDays, beforeDays)
.eq(Vaccine::getNextDate, target));
if (!list.isEmpty()) {
log.info("[提醒] 疫苗 {} 天后到期, 命中 {} 条", beforeDays, list.size());
// TODO: 调用 wx-java 推送订阅消息
}
}
private void scanDeworm(LocalDate today, int beforeDays) {
LocalDate target = today.plusDays(beforeDays);
List<Deworm> list = dewormMapper.selectList(Wrappers.<Deworm>lambdaQuery()
.eq(Deworm::getReminderEnabled, 1)
.eq(Deworm::getRemindBeforeDays, beforeDays)
.eq(Deworm::getNextDate, target));
if (!list.isEmpty()) {
log.info("[提醒] 驱虫 {} 天后到期, 命中 {} 条", beforeDays, list.size());
// TODO: 调用 wx-java 推送订阅消息
}
}
}

View File

@ -0,0 +1,54 @@
package com.pawtrace.server.modules.timemachine.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.result.Result;
import com.pawtrace.server.modules.timemachine.entity.TimeMachineWork;
import com.pawtrace.server.modules.timemachine.service.TimeMachineWorkService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 时光机工坊 Controller
* :核心处理(对比/滤镜/视频合成)在客户端完成,服务端仅保存作品元数据
*
* @author PawTrace Team
*/
@Tag(name = "时光机工坊")
@RestController
@RequestMapping("/timemachine")
public class TimeMachineController {
private final TimeMachineWorkService workService;
public TimeMachineController(TimeMachineWorkService workService) {
this.workService = workService;
}
@Operation(summary = "我的作品列表")
@GetMapping("/list")
public Result<List<TimeMachineWork>> list(@RequestParam(required = false) Integer workType,
@RequestParam(required = false) Long petId) {
return Result.success(workService.lambdaQuery()
.eq(TimeMachineWork::getUserId, StpUtil.getLoginIdAsLong())
.eq(workType != null, TimeMachineWork::getWorkType, workType)
.eq(petId != null, TimeMachineWork::getPetId, petId)
.orderByDesc(TimeMachineWork::getCreateTime)
.list());
}
@Operation(summary = "保存作品")
@PostMapping("/save")
public Result<Boolean> save(@RequestBody TimeMachineWork work) {
work.setUserId(StpUtil.getLoginIdAsLong());
return Result.success(workService.saveOrUpdate(work));
}
@Operation(summary = "删除作品")
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
return Result.success(workService.removeById(id));
}
}

View File

@ -0,0 +1,48 @@
package com.pawtrace.server.modules.timemachine.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 时光机工坊作品
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_timemachine_work")
@Schema(description = "时光机作品")
public class TimeMachineWork extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "作品类型 1同角度对比 2AI滤镜 3时光机视频")
private Integer workType;
@Schema(description = "关联宠物ID")
private Long petId;
@Schema(description = "标题")
private String title;
@Schema(description = "使用的素材URL列表(JSON数组)")
private String sourceUrls;
@Schema(description = "结果URL(图片/视频)")
private String resultUrl;
@Schema(description = "模板标识")
private String templateKey;
@Schema(description = "备注")
private String remark;
}

View File

@ -0,0 +1,8 @@
package com.pawtrace.server.modules.timemachine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.timemachine.entity.TimeMachineWork;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TimeMachineWorkMapper extends BaseMapper<TimeMachineWork> {}

View File

@ -0,0 +1,6 @@
package com.pawtrace.server.modules.timemachine.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.timemachine.entity.TimeMachineWork;
public interface TimeMachineWorkService extends IService<TimeMachineWork> {}

View File

@ -0,0 +1,10 @@
package com.pawtrace.server.modules.timemachine.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.modules.timemachine.entity.TimeMachineWork;
import com.pawtrace.server.modules.timemachine.mapper.TimeMachineWorkMapper;
import com.pawtrace.server.modules.timemachine.service.TimeMachineWorkService;
import org.springframework.stereotype.Service;
@Service
public class TimeMachineWorkServiceImpl extends ServiceImpl<TimeMachineWorkMapper, TimeMachineWork> implements TimeMachineWorkService {}

View File

@ -0,0 +1,35 @@
package com.pawtrace.server.modules.user.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.pawtrace.server.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户 / 鉴权 Controller
*
* @author PawTrace Team
*/
@Tag(name = "用户认证")
@RestController
@RequestMapping("/auth")
public class AuthController {
@Operation(summary = "微信小程序登录(占位)")
@GetMapping("/login")
public Result<String> login() {
// TODO: 接入 wx-java miniapp, 校验 code -> openid -> 自动注册
StpUtil.login(10001L);
return Result.success(StpUtil.getTokenValue());
}
@Operation(summary = "退出登录")
@GetMapping("/logout")
public Result<Void> logout() {
StpUtil.logout();
return Result.success();
}
}

View File

@ -0,0 +1,45 @@
package com.pawtrace.server.modules.user.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.pawtrace.server.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 用户
*
* @author PawTrace Team
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pt_user")
@Schema(description = "用户")
public class User extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "微信openid")
private String openid;
@Schema(description = "微信unionid")
private String unionid;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "性别 0未知 1男 2女")
private Integer gender;
@Schema(description = "手机号")
private String phone;
@Schema(description = "状态 0禁用 1启用")
private Integer status;
}

View File

@ -0,0 +1,14 @@
package com.pawtrace.server.modules.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pawtrace.server.modules.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户 Mapper
*
* @author PawTrace Team
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@ -0,0 +1,17 @@
package com.pawtrace.server.modules.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pawtrace.server.modules.user.entity.User;
/**
* 用户 Service
*
* @author PawTrace Team
*/
public interface UserService extends IService<User> {
/**
* 根据 openid 查询用户
*/
User getByOpenid(String openid);
}

View File

@ -0,0 +1,22 @@
package com.pawtrace.server.modules.user.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pawtrace.server.modules.user.entity.User;
import com.pawtrace.server.modules.user.mapper.UserMapper;
import com.pawtrace.server.modules.user.service.UserService;
import org.springframework.stereotype.Service;
/**
* 用户 Service 实现
*
* @author PawTrace Team
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User getByOpenid(String openid) {
return this.getOne(Wrappers.<User>lambdaQuery().eq(User::getOpenid, openid), false);
}
}

View File

@ -0,0 +1,9 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/pawtrace_dev?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
logging:
level:
com.pawtrace: DEBUG

View File

@ -0,0 +1,10 @@
spring:
datasource:
url: jdbc:mysql://localhost:3306/pawtrace_prod?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
username: pawtrace
password: ${DB_PASSWORD}
logging:
level:
root: WARN
com.pawtrace: INFO

View File

@ -0,0 +1,125 @@
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: server-pawtrace
profiles:
active: dev
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: non_null
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/pawtrace?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
connection-timeout: 30000
max-lifetime: 1800000
data:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 10s
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.pawtrace.server.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: false
global-config:
banner: false
db-config:
id-type: ASSIGN_ID
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
insert-strategy: not_null
update-strategy: not_null
sa-token:
token-name: Authorization
timeout: 2592000
active-timeout: -1
is-concurrent: true
is-share: true
token-style: uuid
is-log: false
is-read-cookie: false
is-read-header: true
wx:
miniapp:
appid: your-appid-here
secret: your-secret-here
msg-data-format: JSON
oss:
type: local
local:
path: ./upload/
domain: http://localhost:8080/api/file/
qiniu:
access-key: your-access-key
secret-key: your-secret-key
bucket: your-bucket
domain: http://your-domain
aliyun:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
bucket-name: your-bucket-name
tencent:
secret-id: your-secret-id
secret-key: your-secret-key
region: ap-guangzhou
bucket-name: your-bucket-name
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: pawtrace
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html
knife4j:
enable: true
setting:
language: zh_cn
logging:
level:
root: INFO
com.pawtrace: DEBUG
com.baomidou.mybatisplus: INFO
file:
name: ./logs/server-pawtrace.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

View File

@ -0,0 +1,251 @@
-- ============================================================
-- 宠迹 PawTrace 数据库初始化脚本
-- 数据库: MySQL 8.x
-- 字符集: utf8mb4 / utf8mb4_unicode_ci
-- ============================================================
CREATE DATABASE IF NOT EXISTS `pawtrace` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `pawtrace`;
-- ------------------------------------------------------------
-- 1. 用户表
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_user`;
CREATE TABLE `pt_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`openid` VARCHAR(64) DEFAULT NULL COMMENT '微信openid',
`unionid` VARCHAR(64) DEFAULT NULL COMMENT '微信unionid',
`nickname` VARCHAR(64) DEFAULT NULL COMMENT '昵称',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
`gender` TINYINT DEFAULT 0 COMMENT '0未知 1男 2女',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`status` TINYINT DEFAULT 1 COMMENT '0禁用 1启用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户';
-- ------------------------------------------------------------
-- 2. 宠物档案
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_pet`;
CREATE TABLE `pt_pet` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`nickname` VARCHAR(64) NOT NULL COMMENT '昵称',
`breed` VARCHAR(64) DEFAULT NULL COMMENT '品种',
`species` TINYINT DEFAULT 1 COMMENT '1狗 2猫 3其他',
`gender` TINYINT DEFAULT 0 COMMENT '0未知 1公 2母',
`birthday` DATE DEFAULT NULL,
`weight` DECIMAL(8,2) DEFAULT NULL COMMENT '当前体重kg',
`avatar` VARCHAR(255) DEFAULT NULL,
`personality_tags` VARCHAR(255) DEFAULT NULL COMMENT '逗号分隔',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物档案';
-- ------------------------------------------------------------
-- 3. 体重历史
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_pet_weight`;
CREATE TABLE `pt_pet_weight` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`pet_id` BIGINT NOT NULL,
`weight` DECIMAL(8,2) NOT NULL,
`record_date` DATE NOT NULL,
`remark` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_pet_id` (`pet_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体重记录';
-- ------------------------------------------------------------
-- 4. 成长日记
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_diary`;
CREATE TABLE `pt_diary` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`pet_id` BIGINT DEFAULT NULL,
`title` VARCHAR(128) DEFAULT NULL,
`content` TEXT,
`media_urls` JSON DEFAULT NULL COMMENT '图片/视频URL数组',
`mood_tags` VARCHAR(255) DEFAULT NULL,
`location` VARCHAR(255) DEFAULT NULL,
`diary_type` TINYINT DEFAULT 1 COMMENT '1日常 2成长 3医疗 4饮食 5趣事',
`happen_date` DATE DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_date` (`user_id`, `happen_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长日记';
-- ------------------------------------------------------------
-- 5. 疫苗
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_vaccine`;
CREATE TABLE `pt_vaccine` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`pet_id` BIGINT NOT NULL,
`vaccine_name` VARCHAR(128) NOT NULL,
`inject_date` DATE DEFAULT NULL,
`next_date` DATE DEFAULT NULL,
`cert_image` VARCHAR(255) DEFAULT NULL,
`reminder_enabled` TINYINT DEFAULT 0,
`remind_before_days` TINYINT DEFAULT 1,
`remark` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_pet_id` (`pet_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疫苗记录';
-- ------------------------------------------------------------
-- 6. 驱虫
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_deworm`;
CREATE TABLE `pt_deworm` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`pet_id` BIGINT NOT NULL,
`drug_name` VARCHAR(128) NOT NULL,
`drug_type` TINYINT DEFAULT 1 COMMENT '1体内 2体外',
`use_date` DATE DEFAULT NULL,
`next_date` DATE DEFAULT NULL,
`drug_image` VARCHAR(255) DEFAULT NULL,
`reminder_enabled` TINYINT DEFAULT 0,
`remind_before_days` TINYINT DEFAULT 1,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_pet_id` (`pet_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='驱虫记录';
-- ------------------------------------------------------------
-- 7. 就诊记录
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_visit_record`;
CREATE TABLE `pt_visit_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`pet_id` BIGINT NOT NULL,
`hospital` VARCHAR(128) DEFAULT NULL,
`reason` VARCHAR(255) DEFAULT NULL,
`diagnosis` TEXT,
`cost` DECIMAL(10,2) DEFAULT NULL,
`visit_date` DATE DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='就诊记录';
-- ------------------------------------------------------------
-- 8. 物资
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_inventory`;
CREATE TABLE `pt_inventory` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`category` TINYINT DEFAULT 6 COMMENT '1粮 2罐头 3零食 4猫砂 5药品 6其他',
`name` VARCHAR(128) NOT NULL,
`stock` DECIMAL(10,2) NOT NULL DEFAULT 0,
`unit` VARCHAR(16) DEFAULT NULL,
`low_stock_threshold` DECIMAL(10,2) DEFAULT 0,
`cover_image` VARCHAR(255) DEFAULT NULL,
`remark` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物资库存';
-- ------------------------------------------------------------
-- 9. 物资消耗记录
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_inventory_log`;
CREATE TABLE `pt_inventory_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`inventory_id` BIGINT NOT NULL,
`user_id` BIGINT NOT NULL,
`quantity` DECIMAL(10,2) NOT NULL,
`remark` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物资消耗';
-- ------------------------------------------------------------
-- 10. 时光机作品
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_timemachine_work`;
CREATE TABLE `pt_timemachine_work` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`work_type` TINYINT NOT NULL COMMENT '1同角度对比 2AI滤镜 3时光机视频',
`pet_id` BIGINT DEFAULT NULL,
`title` VARCHAR(128) DEFAULT NULL,
`source_urls` JSON DEFAULT NULL,
`result_url` VARCHAR(255) DEFAULT NULL,
`template_key` VARCHAR(64) DEFAULT NULL,
`remark` VARCHAR(255) DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时光机作品';
-- ------------------------------------------------------------
-- 11. 生活信息卡
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_life_card`;
CREATE TABLE `pt_life_card` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`category` TINYINT NOT NULL COMMENT '1医院 2餐厅 3公园 4宠物店 5美容',
`name` VARCHAR(128) NOT NULL,
`cover` VARCHAR(255) DEFAULT NULL,
`address` VARCHAR(255) DEFAULT NULL,
`phone` VARCHAR(32) DEFAULT NULL,
`description` TEXT,
`longitude` DECIMAL(10,6) DEFAULT NULL,
`latitude` DECIMAL(10,6) DEFAULT NULL,
`sort_order` INT DEFAULT 0,
`status` TINYINT DEFAULT 1 COMMENT '0下架 1上架',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生活信息卡';
-- ------------------------------------------------------------
-- 12. 反馈
-- ------------------------------------------------------------
DROP TABLE IF EXISTS `pt_feedback`;
CREATE TABLE `pt_feedback` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`fb_type` TINYINT DEFAULT 1,
`content` TEXT NOT NULL,
`contact` VARCHAR(64) DEFAULT NULL,
`images` JSON DEFAULT NULL,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` TINYINT DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='意见反馈';

3
uniapp-pawtrace/.npmrc Normal file
View File

@ -0,0 +1,3 @@
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true

View File

@ -0,0 +1,3 @@
import uniHelper from '@uni-helper/eslint-config'
export default uniHelper()

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="static/logo.svg">
<script>
const coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)')
|| CSS.supports('top: constant(a)'))
document.write(
`<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0${
coverSupport ? ', viewport-fit=cover' : ''}" />`)
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,80 @@
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
'name': '',
'appid': '',
'description': '',
'versionName': '1.0.0',
'versionCode': '100',
'transformPx': false,
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
permissions: [
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.READ_LOGS"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.CAMERA"/>',
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
'<uses-feature android:name="android.hardware.camera"/>',
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
],
},
/* ios打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {},
},
},
/* 快应用特有相关 */
'quickapp': {},
/* 小程序特有相关 */
'mp-weixin': {
appid: '',
setting: {
urlCheck: false,
},
usingComponents: true,
darkmode: true,
themeLocation: 'theme.json',
},
'mp-alipay': {
usingComponents: true,
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'h5': {
darkmode: true,
themeLocation: 'theme.json',
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
})

View File

@ -0,0 +1,89 @@
{
"name": "uniapp-pawtrace",
"type": "module",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "unh dev",
"build": "unh build",
"about": "unh info",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-5000720260410001",
"@dcloudio/uni-app-harmony": "3.0.0-5000720260410001",
"@dcloudio/uni-app-plus": "3.0.0-5000720260410001",
"@dcloudio/uni-components": "3.0.0-5000720260410001",
"@dcloudio/uni-h5": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-alipay": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-baidu": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-harmony": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-jd": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-lark": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-qq": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-toutiao": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
"@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
"@uni-helper/uni-network": "^0.23.1",
"@uni-helper/uni-promises": "^0.2.1",
"@uni-helper/uni-use": "^0.19.17",
"@vueuse/core": "9.13.0",
"echarts": "^6.0.0",
"pinia": "2.2.4",
"uni-echarts": "^2.5.1",
"uview-pro": "^0.5.17",
"vue": "3.4.21",
"vue-i18n": "9.6.2",
"vue-router": "4.5.1",
"z-paging": "^2.8.8"
},
"devDependencies": {
"@binbinji/vite-plugin-component-placeholder": "^0.0.15",
"@dcloudio/types": "3.4.28",
"@dcloudio/uni-automator": "3.0.0-5000720260410001",
"@dcloudio/uni-cli-shared": "3.0.0-5000720260410001",
"@dcloudio/uni-stacktracey": "3.0.0-5000720260410001",
"@dcloudio/vite-plugin-uni": "3.0.0-5000720260410001",
"@iconify-json/carbon": "^1.2.20",
"@mini-types/alipay": "^3.0.14",
"@types/node": "^25.6.0",
"@uni-helper/eslint-config": "^0.7.1",
"@uni-helper/plugin-uni": "0.1.0",
"@uni-helper/unh": "^0.3.1",
"@uni-helper/uni-types": "^1.0.0-alpha.8",
"@uni-helper/unocss-preset-uni": "^0.2.11",
"@uni-helper/vite-plugin-uni-components": "^0.2.10",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.11",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.12",
"@uni-helper/vite-plugin-uni-pages": "^0.3.24",
"@uni-helper/vite-plugin-uni-platform": "^0.0.5",
"@uni-ku/root": "^1.4.1",
"@vue/runtime-core": "3.4.21",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.2.1",
"miniprogram-api-typings": "^5.1.2",
"sass": "1.64.2",
"typescript": "5.9.3",
"unocss": "66.0.0",
"vite": "5.2.8",
"vitest": "^4.1.4",
"vitest-environment-uniapp": "^0.0.5",
"vue-tsc": "^3.2.7"
},
"pnpm": {
"overrides": {
"unconfig": "7.3.2"
}
},
"overrides": {
"unconfig": "7.3.2"
},
"resolutions": {
"unconfig": "7.3.2"
}
}

View File

@ -0,0 +1,16 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
pages: [],
globalStyle: {
backgroundColor: '@bgColor',
backgroundColorBottom: '@bgColorBottom',
backgroundColorTop: '@bgColorTop',
backgroundTextStyle: '@bgTxtStyle',
navigationBarBackgroundColor: '#000000',
navigationBarTextStyle: '@navTxtStyle',
navigationBarTitleText: 'Uni Creator',
navigationStyle: 'custom',
},
subPackages: [],
})

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