初始化代码
This commit is contained in:
commit
cec5498060
27
.dockerignore
Normal file
27
.dockerignore
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 构建上下文排除项,加速构建并避免敏感文件泄漏
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
.mvn/wrapper/maven-wrapper.properties
|
||||||
|
|
||||||
|
# IDE & 系统
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# 文档(不需要进入镜像)
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
src/test/
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
*.original
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
.vscode/
|
||||||
|
.settings/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Local config
|
||||||
|
application-local.yml
|
||||||
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
wrapperVersion=3.3.4
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||||
11
.run/LaopAdminApplication.run.xml
Normal file
11
.run/LaopAdminApplication.run.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="LaopAdminApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
|
||||||
|
<option name="ACTIVE_PROFILES" />
|
||||||
|
<module name="LAOP-admin" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.haisen.laop.LaopAdminApplication" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="file://$MODULE_WORKING_DIR$" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
63
Dockerfile
Normal file
63
Dockerfile
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# =====================================================================
|
||||||
|
# 翼云Hub · LAOP-admin 后端镜像 Dockerfile
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# 说明:
|
||||||
|
# - 项目 pom.xml 指定 Java 17(source/target),编译期和运行期版本严格一致,
|
||||||
|
# - jar 包最终名为 laop-admin.jar(由 pom.xml finalName 决定)。
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 阶段1:构建期(maven + jdk17,与 pom.xml 严格对齐)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
FROM maven:3.9-eclipse-temurin-17 AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# 先单独 COPY pom.xml,利用 Docker 缓存层加速依赖下载
|
||||||
|
COPY pom.xml ./
|
||||||
|
# 预下载依赖(仅当 pom 变化时重新执行,提升后续构建速度)
|
||||||
|
RUN mvn dependency:go-offline -B
|
||||||
|
|
||||||
|
# 复制源码
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
# 打包:激活 prod profile,让 application.yml 中的 @spring.profiles.active@ 替换为 prod
|
||||||
|
# -DskipTests:构建镜像时跳过测试,测试应在 CI 流水线单独执行
|
||||||
|
RUN mvn clean package -DskipTests -Pprod
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 阶段2:运行期(JRE 17 LTS)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
FROM eclipse-temurin:17-jre-jammy
|
||||||
|
|
||||||
|
LABEL maintainer="laop-admin"
|
||||||
|
LABEL description="翼云Hub · 无人机信息开放平台 后端服务"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 时区设置为中国时区(与 application.yml 中 jackson.time-zone 一致)
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
# 预创建静态文件挂载目录 /data/static
|
||||||
|
# - compose 通过 volumes 挂载宿主机 /home/laop/admin 到此处,持久化上传文件
|
||||||
|
# - 这里显式创建是为了:即使未挂载(如直接 docker run 调试),代码写文件也不会因目录缺失失败
|
||||||
|
# - 同时通过 ENV LAOP_STATIC_DIR=/data/static 给镜像内置默认值,与 compose 挂载点对齐
|
||||||
|
RUN mkdir -p /data/static
|
||||||
|
ENV LAOP_STATIC_DIR=/data/static
|
||||||
|
|
||||||
|
# 拷贝构建产物
|
||||||
|
COPY --from=builder /build/target/laop-admin.jar app.jar
|
||||||
|
|
||||||
|
# 容器内暴露端口(与 application.yml server.port=8081 一致)
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
# JVM 参数:容器内内存限制感知 + GC 优化(2核8G 服务器)
|
||||||
|
# -XX:+UseContainerSupport:让 JVM 感知 cgroup 限制(JDK10+ 默认开启,显式声明)
|
||||||
|
# -XX:MaxRAMPercentage=75.0:限制堆内存占容器内存 75%,给非堆留空间
|
||||||
|
# -Djava.security.egd=file:/dev/./urandom:加速 SecureRandom 启动
|
||||||
|
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"
|
||||||
|
|
||||||
|
# 运行:显式指定 prod profile,覆盖任何默认配置
|
||||||
|
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar --spring.profiles.active=prod"]
|
||||||
997
PRD-无人机信息开放平台小程序.md
Normal file
997
PRD-无人机信息开放平台小程序.md
Normal file
|
|
@ -0,0 +1,997 @@
|
||||||
|
# 翼云Hub · 产品文档(PRD)
|
||||||
|
|
||||||
|
> **Slogan**:全链路低空体系聚合,打破供需两端信息壁垒。
|
||||||
|
|
||||||
|
> **版本 v1.0** · 待开发
|
||||||
|
> **主色**:`#5E46F6`(深紫主色)/ `#7C6AF8`(辅)/ `#F2EFFE`(底)/ `#FFFFFF`(卡)/ `#F59E0B`(金色点缀)
|
||||||
|
> **导航**:4 个底部一级 Tab(**首页 / 任务中心 / 研学与课程 / 个人中心**)
|
||||||
|
> **参考原型**:图2(首页结构)· 图3(个人中心结构)· 图1(功能菜单来源)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 修订记录
|
||||||
|
|
||||||
|
| 版本 | 日期 | 主要变更 |
|
||||||
|
|------|------|----------|
|
||||||
|
| v1.0 | 待开发 | 初版产品需求文档,以图 1 功能菜单为蓝本,4 Tab 结构(首页 / 任务中心 / 研学与课程 / 个人中心) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与机会
|
||||||
|
|
||||||
|
### 1.1 背景
|
||||||
|
国内民用无人机产业进入"持证合规 + 商业落地"双驱动的爆发期。CAAC(民用无人驾驶航空器综合管理平台)2024 年起强制要求超 250g 机型实名注册、操控员持证;植保、巡检、航拍、表演四大场景的商业订单快速放量,但供需两端存在明显错配。
|
||||||
|
|
||||||
|
- **需求侧(商家/客户)**:临时用机、航拍交付、植保作业、应急巡检,缺可信飞手,缺合同/合规保障
|
||||||
|
- **供给侧(飞手)**:持证后无稳定接单渠道,资质难被看见
|
||||||
|
- **服务端(培训机构、维修点、保险公司、租赁商)**:缺精准流量
|
||||||
|
|
||||||
|
行业缺少一个"**开放聚合 + 资质可信 + 交易闭环**"的信息平台。
|
||||||
|
|
||||||
|
### 1.2 目标(北极星指标)
|
||||||
|
|
||||||
|
| 阶段 | 目标 | 北极星指标 |
|
||||||
|
|------|------|------------|
|
||||||
|
| M3 | 上线跑通 | 认证飞手数 ≥ 3,000;认证商家数 ≥ 500 |
|
||||||
|
| M6 | 双边活跃 | 月成交任务数 ≥ 2,000;月活飞手 ≥ 5,000 |
|
||||||
|
| M12 | 行业心智 | CAAC 报名转化率行业第一;"找飞手"心智占位 |
|
||||||
|
|
||||||
|
### 1.3 用户角色
|
||||||
|
|
||||||
|
| 角色 | 占比 | 核心诉求 | 关键场景 |
|
||||||
|
|------|------|----------|----------|
|
||||||
|
| **C1 飞手** | 50% | 接单变现、资质展示、考证指引 | 任务中心接单、飞手中心、刷题 |
|
||||||
|
| **C2 商家/客户** | 30% | 发布需求、找可信飞手、合规交付 | 任务中心发单、查附近、CAAC 报名 |
|
||||||
|
| **C3 培训机构** | 10% | 招生、课程展示、品牌曝光 | 商家认证、题库合作 |
|
||||||
|
| **C4 服务商** | 10% | 流量、订单分发 | 商家认证 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 导航与功能架构
|
||||||
|
|
||||||
|
### 2.1 底部 Tab(4 个)
|
||||||
|
|
||||||
|
| 序号 | Tab | 定位 | 核心目的 |
|
||||||
|
|------|-----|------|----------|
|
||||||
|
| ① | **首页** | 流量分发中枢 | 黄金位 + 9 宫格 + 供需双广场 + 优质任务 1 条 |
|
||||||
|
| ② | **任务中心** | 交易主战场 | 任务列表 + 多维度筛选 + 抢单 |
|
||||||
|
| ③ | **研学与课程** | 内容/服务留存 | 课程展示 + 预约 |
|
||||||
|
| ④ | **个人中心** | 身份与认证 | 我的任务 + 飞手/商家双身份认证 + 推广 |
|
||||||
|
|
||||||
|
### 2.2 全量功能清单(按图 1 + 业务扩展)
|
||||||
|
|
||||||
|
| 一级 Tab | 二级入口 | 三级 / 页面 | 价值 | 优先级 |
|
||||||
|
|----------|----------|-------------|------|--------|
|
||||||
|
| **首页** | 城市定位 | 定位选择 | LBS 入口 | P0 |
|
||||||
|
| | 顶部轮播 | CAAC 报名 / 招商 / 表演 | 运营位 | P0 |
|
||||||
|
| | 公告条 | 最新政策 / 新闻 | 内容置顶 | P0 |
|
||||||
|
| | 9 宫格金刚区(详见 2.3) | 9 个入口 | 流量分发 | P0 |
|
||||||
|
| | **供需双广场**(独立菜单) | 商家发单 · 飞手接单 | **核心交易入口** | **P0** |
|
||||||
|
| | **优质任务**(仅 1 张) | 首页露出 1 张顶部任务 | 转化错点 | P0 |
|
||||||
|
| **任务中心** | 筛选按钮 | 推荐 / 附近 / 高价 / 长期 | 排序入口 | P0 |
|
||||||
|
| | **⚙ 筛选按钮** | 7 组 / 55 个筛选项(右侧抽屉) | 精准筛选 | P0 |
|
||||||
|
| | 任务列表(横向大卡) | 1 优质 + 多普通任务 | 抢单入口 | P0 |
|
||||||
|
| | 任务详情 | 详情页 + IM + 接单 | 成交 | P0 |
|
||||||
|
| **研学与课程** | 分类 Tab | 全部 / 认知启蒙 / 技能进阶 / 创新应用 / 竞赛辅导 | 内容导航 | P0 |
|
||||||
|
| | 课程列表 | 卡片列表(6门真实案例课程) | 内容/服务留存 | **P0** |
|
||||||
|
| | 课程详情 | 课程信息 + 预约入口 | 深度了解 | P0 |
|
||||||
|
| | 研学预约 | 填写信息提交预约 | 核心转化 | **P0** |
|
||||||
|
| **个人中心** | 我的任务(3 态) | 待接单 / 进行中 / 已完成 | 任务管理 | **P0** |
|
||||||
|
| | 飞手推广 | 老带新得积分 | 拉新 | P0 |
|
||||||
|
| | 认证与工具 | 飞手认证 / 商家认证 / 我的发布 / 实名认证 | **身份门槛** | **P0** |
|
||||||
|
| | | 飞手中心 / 商家中心 | 身份聚合 | P0 |
|
||||||
|
|
||||||
|
|
||||||
|
### 2.3 首页 9 宫格金刚区(9 个入口)
|
||||||
|
|
||||||
|
| # | 入口 | 跳转 | 备注 |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 1 | CAAC 报名 | 报名页 | 强引流,潜在客转化 |
|
||||||
|
| 2 | 无人机表演 | 表演列表 | 高客单 |
|
||||||
|
| 3 | 实力商家 | 商家列表 | 信任锚点 |
|
||||||
|
| 4 | 本地飞手 | 飞手列表 | 信任锚点 |
|
||||||
|
| 5 | 培训考试 | 培训模块 | 留存 + 引流 |
|
||||||
|
| 6 | 发布任务 | 发布任务页(需商家认证) | 商家侧入口 |
|
||||||
|
| 7 | 我要接单 | 任务中心(需飞手认证) | 飞手侧入口 |
|
||||||
|
| 8 | 飞手推广 | 推广页(带 HOT 角标) | 拉新 |
|
||||||
|
| 9 | **滴滴打无人机** | 调度页(类打车模式) | **新功能·核心亮点** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 功能关联分析(核心)
|
||||||
|
|
||||||
|
### 3.1 三大 Tab 关联总览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ 首页(流量分发中枢) │
|
||||||
|
│ 轮播位 ─→ 强运营:CAAC报名 / 表演 / 招商 │
|
||||||
|
│ 公告 ─→ 政策更新、行业新闻(点击跳行业动态详情) │
|
||||||
|
│ 金刚区(9 宫格) ─→ 功能分发 │
|
||||||
|
│ ├─ CAAC报名 ──→ 飞手认证 ──→ 训练题库 │
|
||||||
|
│ ├─ 无人机表演 ──→ 任务中心(高客单任务) │
|
||||||
|
│ ├─ 实力商家 ──→ 商家认证入口(个人中心) │
|
||||||
|
│ ├─ 本地飞手 ──→ 飞手认证入口(个人中心) │
|
||||||
|
│ ├─ 培训考试 ──→ 附近商家 + 题库刷题 │
|
||||||
|
│ ├─ 发布任务 ──→ 商家认证校验(个人中心) │
|
||||||
|
│ └─ 我要接单 ──→ 飞手认证校验(个人中心) │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
研学与课程 个人中心 · 身份
|
||||||
|
(内容留存) 飞手 ←─→ 商家
|
||||||
|
│ │
|
||||||
|
└──────────┬────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ 任务中心 │
|
||||||
|
│ 商家发单/飞手接单│
|
||||||
|
│ (需对应身份认证)│
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 关键功能关联(10 条)
|
||||||
|
|
||||||
|
| # | 触发点 | 关联点 | 业务价值 |
|
||||||
|
|---|--------|--------|----------|
|
||||||
|
| 1 | CAAC 报名点击 | → 飞手认证引导页 | 流量 → 认证漏斗 |
|
||||||
|
| 2 | 实力商家 / 本地飞手 | → 详情页 + IM + 收藏 | 信任 → 咨询 → 成交 |
|
||||||
|
| 3 | 培训考试题库 | → 错题 → 课程推荐 | 学习 → 报名 → 机构 |
|
||||||
|
| 4 | 商家发单 | → 校验商家认证状态 | 未认证阻断 + 引导 |
|
||||||
|
| 5 | 飞手接单 | → 校验飞手认证 + 保险 | 风控前置 |
|
||||||
|
| 6 | 任务中心"优质任务"标签 | → 付费订阅 / 会员 | GMV 提升 |
|
||||||
|
| 7 | 飞手推广 | → 老带新积分 | 拉新 |
|
||||||
|
| 8 | 附近商家 | → 定位授权 + 商家详情 | LBS 转化 |
|
||||||
|
| 9 | 研学课程详情 | → 相关任务 / 商家挂载 | 内容 → 交易 |
|
||||||
|
| 11 | 滴滴打无人机 | → 无人机飞手网络 → 配送任务派单 | 新增核心功能 |
|
||||||
|
| 10 | 资质中心 | → 过期提醒 + 续期 | 长期留存 |
|
||||||
|
|
||||||
|
### 3.3 首页信息流(自上而下)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 状态栏
|
||||||
|
2. 城市定位(如:全国 / 上海)+ 消息/设置
|
||||||
|
3. 轮播图(CAAC 报名 / 招商 / 表演)
|
||||||
|
4. 公告条(最新政策 / 新闻)
|
||||||
|
5. 9 宫格金刚区(9 个入口,详见 2.3)
|
||||||
|
6. 供需双广场(独立菜单 · 仿图 1)
|
||||||
|
左卡:📤 商家发单 │ 右卡:🛩 飞手接单
|
||||||
|
7. 滴滴打无人机(同城即时配送)
|
||||||
|
地图动画示意(无人机飞手轨迹)
|
||||||
|
配送流程 4 步:骑手取货 → 无人机起飞 → 配送中 → 预计到达
|
||||||
|
底部快捷下单栏:30 分钟必达 · 支持紧急件
|
||||||
|
8. 研学体系(首页展示课程 · 2~3 张横向卡)
|
||||||
|
① [两天一晚世园无人机研学课程] ¥1,388/期
|
||||||
|
② [无人机技能进阶营(3-5天)] ¥2,680/期
|
||||||
|
③ [鄂尔多斯雏鹰计划] ¥3,200/期
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 任务中心 Tab
|
||||||
|
|
||||||
|
```
|
||||||
|
顶部筛选按钮栏:
|
||||||
|
[⚙ 筛选▾] [📍 附近] ← 推荐/高价/长期 已合并到筛选面板
|
||||||
|
|
||||||
|
点击 [⚙ 筛选] → 右侧抽屉式筛选面板(7 组 / 55 个筛选项):
|
||||||
|
· 任务分类(7) 全部 / 植保 / 航拍 / 巡检 / 表演 / 吊运 / 测绘
|
||||||
|
· 任务状态(5) 全部 / 待接单 / 已接单 / 执行中 / 已完成
|
||||||
|
· 价格预算(5+输入)不限 / 0-3千 / 3-10千 / 1-5万 / 5万以上 + 自定义
|
||||||
|
· 所在地区(9) 全国 + 8 省市
|
||||||
|
· 人员数量(5) 不限 / 1人 / 2人 / 3-5人 / 5人以上
|
||||||
|
· 发布时间(5) 全部 / 今天 / 3天内 / 本周内 / 本月内
|
||||||
|
· 其他筛选(5) 优质任务 / 含飞手险 / 长期合作 / 急单 / 团队单
|
||||||
|
|
||||||
|
主体任务卡(仿图 1 横向大卡):
|
||||||
|
· 头部:标签(分类 + 优质 + 状态)+ 地点
|
||||||
|
· 标题:粗体任务名
|
||||||
|
· 左图右信息卡:地点 / 人数 / 时间
|
||||||
|
· 底部:价格(左侧大字号)+ 操作按钮(查看附近飞手 / 去抢单)
|
||||||
|
· 描述:飞手险 + 任务详情(实景文字)
|
||||||
|
|
||||||
|
优质任务强化(仅首卡):
|
||||||
|
· 金色边框 + 米黄渐变背景 + 金色 ★ 徽标
|
||||||
|
· 金色价格 + 金色 CTA 按钮
|
||||||
|
· 右上角金色光晕
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 滴滴打无人机 Tab(独立 Tab)
|
||||||
|
|
||||||
|
> 核心定位:同城即时配送平台,配送端为无人机(类比美团/闪送,骑手换无人机)
|
||||||
|
|
||||||
|
```
|
||||||
|
核心业务流程:
|
||||||
|
用户下单 → 系统派单给附近无人机 → 无人机取货起飞 → 配送中(实时轨迹)→ 送达
|
||||||
|
|
||||||
|
功能清单:
|
||||||
|
· 一键下单:填写取货/收货地址 + 物品重量/类型 → 生成订单
|
||||||
|
· 配送地图:无人机轨迹实时展示(起飞点→配送中→到达)
|
||||||
|
· 配送流程:4 步追踪(骑手取货 / 无人机起飞 / 配送中 / 预计到达)
|
||||||
|
· 计价规则:按距离 + 重量 + 时效(普通件/加急件)
|
||||||
|
· 配送范围:同城 30 分钟必达,支持紧急件
|
||||||
|
· 飞手/商家入驻:认证后接入配送网络
|
||||||
|
|
||||||
|
展示 4 步配送流程示例(首页版):
|
||||||
|
① ✓ 骑手取货 09:14
|
||||||
|
② ✓ 无人机起飞 09:16 [T100 起飞]
|
||||||
|
③ 🚁 配送中 09:17 [飞行高度 120m · 时速 60km/h]
|
||||||
|
④ ◎ 预计到达 09:25 [送货上门]
|
||||||
|
|
||||||
|
底部下单栏:30 分钟内送达 · 支持紧急件 [立即下单]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 研学与课程 Tab
|
||||||
|
|
||||||
|
```
|
||||||
|
分类 Tab:全部 / 认知启蒙 / 技能进阶 / 创新应用 / 竞赛辅导
|
||||||
|
|
||||||
|
课程卡片字段:
|
||||||
|
· 标签(热门/推荐/新开)+ 分类标签 + 对象年龄
|
||||||
|
· 课程名 + 地点 + 时长
|
||||||
|
· 课程亮点(3项展开)
|
||||||
|
· 价格 + [立即预约] 按钮
|
||||||
|
|
||||||
|
展示 6 门真实课程案例:
|
||||||
|
① 两天一晚世园无人机研学课程(北京)¥1,388/期 [热门·青少年·认知启蒙]
|
||||||
|
② 无人机技能进阶营(3-5天)¥2,680/期 [技能进阶·初中生]
|
||||||
|
③ 鄂尔多斯科技馆「雏鹰计划」(8天16课)¥3,200 [创新应用·高中生]
|
||||||
|
④ 「越州蒲公英计划」青少年无人机研学(绍兴)¥680 [竞赛辅导·青少年]
|
||||||
|
⑤ 「智趣少年营」无人机兴趣课程(社区)¥380 [趣味科普·零基础]
|
||||||
|
⑥ 「小小农发人」无人机农业研学之旅(简阳)¥280 [农业应用·亲子]
|
||||||
|
|
||||||
|
预约交互:[立即预约] → 弹出预约表单(姓名/手机/人数/备注)→ 提交成功提示
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 个人中心信息流(自上而下)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 顶部渐变区(无右上角图标):
|
||||||
|
👤 头像 + 登录/注册按钮
|
||||||
|
2. 我的任务(3 态):
|
||||||
|
[📋 待接单 2] [🛩 进行中 1] [✓ 已完成]
|
||||||
|
3. 飞手推广 Banner:
|
||||||
|
📢 邀请飞手认证 · 认证得积分 · 积分可提现 [去推广 ›]
|
||||||
|
4. 认证与工具:
|
||||||
|
上排 4:[飞手认证] [商家认证] [我的发布] [实名认证]
|
||||||
|
下排 2:[🏆飞手中心] [🏬商家中心]
|
||||||
|
```
|
||||||
|
|
||||||
|
> ❌ 已删除模块:我的服务(优惠券/商家发货/分销绑定/收货地址)、信息完善、飞手资质、商家资质、功能关联导览、个人中心右上角 3 图标(App/消息/设置)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 核心流程
|
||||||
|
|
||||||
|
### 4.1 飞手入驻
|
||||||
|
注册 / 微信一键登录 → 飞手认证(上传 CAAC 执照 + 身份证 + 机型)→ 审核(1 工作日)→ 通过 → 进入任务中心接单
|
||||||
|
|
||||||
|
### 4.2 商家发单
|
||||||
|
注册 → 商家认证(营业执照 + 法人身份证 + 经营范围)→ 审核 → 通过 → 任务中心 → 发布任务(类型 / 地点 / 时间 / 预算 / 机型要求)→ 飞手接单 → 双方 IM 确认 → 线下执行 → 平台评价 → 抽佣结算
|
||||||
|
|
||||||
|
### 4.3 任务状态机
|
||||||
|
```
|
||||||
|
待接单 → 已接单(待执行)→ 执行中 → 待验收 → 已完成
|
||||||
|
↓
|
||||||
|
异常:取消 / 申诉 / 退款
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 认证状态机
|
||||||
|
```
|
||||||
|
未认证 → 资料上传中 → 审核中(≤24h)→ 已认证(有效 1 年)→ 即将过期(30 天)→ 过期
|
||||||
|
↓
|
||||||
|
续期申请
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 筛选交互流程
|
||||||
|
点击 [⚙ 筛选] → 右侧抽屉滑入 → 7 组筛选项可点击切换 → 自定义价格输入 → [重置全部] 或 [确认筛选 (12)] → 关闭抽屉 → 列表刷新 + 提示"筛选已应用"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 信息架构
|
||||||
|
|
||||||
|
### 5.1 页面清单(28 个)
|
||||||
|
|
||||||
|
| 模块 | 页面 |
|
||||||
|
|------|------|
|
||||||
|
| 通用 | 启动页、登录/注册、消息中心、定位选择 |
|
||||||
|
| 首页 | 首页、新闻列表、新闻详情、CAAC 报名、无人机表演列表 / 详情 |
|
||||||
|
| 商家 / 飞手 | 实力商家列表 / 详情、本地飞手列表 / 详情 |
|
||||||
|
| 培训 | 培训考试首页、附近商家(培训)、题库刷题、错题本 |
|
||||||
|
| 任务中心 | 任务中心首页、发布任务、我要接单、任务详情、任务管理、筛选面板 |
|
||||||
|
| 研学与课程 | 列表(全部 / 认知启蒙 / 技能进阶 / 创新应用 / 竞赛辅导)、详情、预约 |
|
||||||
|
| 我的 | 我的首页(任务 3 态)、飞手推广、飞手认证、商家认证、我的发布、实名认证、飞手中心、商家中心、关于我们 |
|
||||||
|
|
||||||
|
### 5.2 底部 Tab(4 个 · 按图 1 一级菜单)
|
||||||
|
|
||||||
|
- **首页**:流量分发
|
||||||
|
- **任务中心**:核心交易(独立 Tab · 必到)
|
||||||
|
- **行业动态**:资讯流
|
||||||
|
- **个人中心**:身份与认证
|
||||||
|
|
||||||
|
> 与图 1 一级菜单对应关系:图 1 的"首页" = 本文档的首页 + 任务中心(交易功能下沉到 Tab);图 1 的"行业动态" = 研学与课程 Tab;图 1 的"个人中心" = 同名 Tab。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. UI 规范
|
||||||
|
|
||||||
|
### 6.1 色彩系统
|
||||||
|
|
||||||
|
| 用途 | 颜色 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| **主色** | **`#5E46F6`** = `rgb(94,70,246)` | 品牌色 · 按钮 / Tag / 强调 / Tab 选中 / 阴影 |
|
||||||
|
| 辅助紫 | `#7C6AF8` | 次级按钮、渐变起点 |
|
||||||
|
| 浅紫底 | `#F2EFFE` | 页面背景、Tab 浅态 |
|
||||||
|
| 卡片白 | `#FFFFFF` | 内容卡 |
|
||||||
|
| 文字主 | `#1A1730` | 标题 |
|
||||||
|
| 文字次 | `#5E5870` | 描述 |
|
||||||
|
| 文字弱 | `#9D97AE` | 提示 |
|
||||||
|
| 成功 | `#22C55E` | 认证通过 |
|
||||||
|
| 警告 | `#F59E0B` | 即将过期 |
|
||||||
|
| 错误 | `#EF4444` | 失败 |
|
||||||
|
| **金色点缀** | **`#F59E0B`** | **优质任务标签 / 价格 / CTA / 边框** |
|
||||||
|
|
||||||
|
### 6.2 字体
|
||||||
|
- iOS:PingFang SC
|
||||||
|
- Android:系统默认
|
||||||
|
- 字号阶梯:11 / 12 / 13 / 14 / 15 / 17 / 20 / 24 / 32
|
||||||
|
|
||||||
|
### 6.3 圆角 / 阴影
|
||||||
|
- 卡圆角:12-16 px
|
||||||
|
- 按钮圆角:8-14 px
|
||||||
|
- 头像圆角:50%
|
||||||
|
- 卡阴影:`0 2px 12px rgba(94,70,246,0.10)` ——`--shadow`
|
||||||
|
- 任务中心卡阴影:`0 4px 16px rgba(94,70,246,0.12)` ——`--shadow-md`
|
||||||
|
- 大阴影(轮播 / Banner):`0 8px 24px rgba(94,70,246,0.18)` ——`--shadow-lg`
|
||||||
|
- 优质任务金色阴影:`0 6px 20px rgba(245,158,11,0.22)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 关键指标
|
||||||
|
|
||||||
|
| 指标 | 目标(M3) | 监控位 |
|
||||||
|
|------|------------|--------|
|
||||||
|
| CAAC 报名点击 → 提交转化率 | ≥ 35% | 首页轮播点击 |
|
||||||
|
| 飞手认证完成率 | ≥ 60% | 引导页漏斗 |
|
||||||
|
| 任务中心点击率 | ≥ 50% | 首页"供需双广场" + "优质任务" |
|
||||||
|
| 优质任务点击 → 接单 | ≥ 20% | 任务中心首卡 |
|
||||||
|
| 任务平均成交时长 | ≤ 4h | 任务详情 |
|
||||||
|
| 7 日留存 | ≥ 30% | 启动埋点 |
|
||||||
|
| 研学与课程 Tab PV | 首页 PV 的 30% | Tab 切换埋点 |
|
||||||
|
| 筛选面板使用率 | ≥ 15% | 任务中心 Tab |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 风险与对策
|
||||||
|
|
||||||
|
| 风险 | 等级 | 对策 |
|
||||||
|
|------|------|------|
|
||||||
|
| 飞手资质造假 | 高 | 接入 CAAC 接口核验执照真伪 + 履约保证金 |
|
||||||
|
| 商家拖欠 / 跑单 | 高 | 平台担保支付 + 评价 + 黑名单 |
|
||||||
|
| 飞手人身安全 | 中 | 保险合作 + 紧急 SOS 按钮 + 行程共享 |
|
||||||
|
| 监管政策变化 | 中 | 法务对接民航局,配置审核人力 |
|
||||||
|
| 冷启动供需 | 中 | 地推:先与 3-5 家培训机构、2-3 个无人机协会合作 |
|
||||||
|
| 优质任务供给不足 | 中 | 平台补贴 + 头部商家 BD |
|
||||||
|
| 首页任务中心被弱化 | 中 | 顶部"优质任务"金色强化 + 供需双广场独立菜单导流 |
|
||||||
|
| 低空配送合规(飞行审批/空域限制) | 高 | 对接 UOM 平台获取飞行许可;设置电子围栏自动拦截禁飞区订单 |
|
||||||
|
| 配送延误/事故(天气/机械故障) | 中 | 无人机状态实时监控;天气预警自动暂停;备用机调度机制 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 版本规划
|
||||||
|
|
||||||
|
| 版本 | 时间 | 内容 |
|
||||||
|
|------|------|------|
|
||||||
|
| v1.0 | 待开发 | 首页 + 任务中心 + 研学与课程 + 个人中心,4 Tab 完整功能 |
|
||||||
|
| v2.0 | M3 | 培训题库 + 保险合作 + 智能匹配 |
|
||||||
|
| v3.0 | M6+ | 行业 SaaS + AR 展示 + 地图找飞手 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 附录:与原图功能对照表
|
||||||
|
|
||||||
|
### 10.1 一级菜单对照
|
||||||
|
|
||||||
|
| 图 1 一级菜单 | 本文档 Tab | 关键二级 |
|
||||||
|
|--------------|-----------|----------|
|
||||||
|
| 首页 | **首页 + 任务中心** | 9 宫格 + 供需双广场 + 滴滴打无人机 + 研学体系 |
|
||||||
|
| 行业动态 | **研学与课程** | 列表 + 详情 + 预约(6 门课程) |
|
||||||
|
| 个人中心 | **个人中心** | 任务 3 态 + 飞手 / 商家双身份认证 + 推广 |
|
||||||
|
|
||||||
|
### 10.2 二级菜单对照
|
||||||
|
|
||||||
|
| 图 1 二级菜单 | 本文档对应 | 实现位置 |
|
||||||
|
|--------------|------------|----------|
|
||||||
|
| 新闻咨询 | ✓ | 首页轮播 + 公告(研学与课程内容) |
|
||||||
|
| CAAC 报名 | ✓ | 首页 9 宫格 #1 + 独立页 |
|
||||||
|
| 无人机表演 | ✓ | 首页 9 宫格 #2 + 独立板块 |
|
||||||
|
| 实力商家 | ✓ | 首页 9 宫格 #3 + 商家列表 |
|
||||||
|
| 本地飞手 | ✓ | 首页 9 宫格 #4 + 飞手列表 |
|
||||||
|
| 培训考试 | ✓ | 首页 9 宫格 #5 + 培训模块 |
|
||||||
|
| **供需双广场** | ✓(**独立菜单**) | 首页 6 区(左:商家发单 / 右:飞手接单) |
|
||||||
|
| **优质任务** | ✓ | 首页 7 区(仅 1 张金色强化卡)+ 任务中心首卡 |
|
||||||
|
| 信息完善 | ❌(已删除) | 个人中心(合并到飞手 / 商家认证流程中) |
|
||||||
|
| 飞手认证 | ✓ | 个人中心「认证与工具」上排 #1 |
|
||||||
|
| 飞手资质中心 | ❌(已删除) | 资质合并到飞手认证页面 |
|
||||||
|
| 商家认证 | ✓ | 个人中心「认证与工具」上排 #2 |
|
||||||
|
| 商家资质中心 | ❌(已删除) | 资质合并到商家认证页面 |
|
||||||
|
| 关于我们 | ✓ | 个人中心底部 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 数据库设计(DDL)
|
||||||
|
|
||||||
|
> 数据库类型:MySQL 8.0
|
||||||
|
> 字符集:`utf8mb4` / 排序规则:`utf8mb4_unicode_ci`
|
||||||
|
> 所有表均含 `created_at` / `updated_at` 通用字段;主键均使用 `BIGINT` 自增;软删除用 `deleted_at`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.1 公告表 `announcement`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| title | VARCHAR(100) | 公告标题 | NOT NULL |
|
||||||
|
| content | TEXT | 公告正文 | |
|
||||||
|
| valid_from | DATETIME | 生效开始时间 | |
|
||||||
|
| valid_to | DATETIME | 生效结束时间 | |
|
||||||
|
| status | TINYINT | 状态:0隐藏 1显示 | DEFAULT 1 |
|
||||||
|
| sort_order | INT | 排序,越小越靠前 | DEFAULT 0 |
|
||||||
|
| publisher_id | BIGINT | 发布人ID(关联user.id) | |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE announcement (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
title VARCHAR(100) NOT NULL COMMENT '公告标题',
|
||||||
|
content TEXT COMMENT '公告正文',
|
||||||
|
valid_from DATETIME COMMENT '生效开始时间',
|
||||||
|
valid_to DATETIME COMMENT '生效结束时间',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:0隐藏 1显示',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
publisher_id BIGINT COMMENT '发布人ID',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
INDEX idx_status_sort (status, sort_order),
|
||||||
|
INDEX idx_valid_period (valid_from, valid_to)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公告表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.2 任务表 `task`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| task_no | VARCHAR(32) | 任务编号(全局唯一) | UNIQUE |
|
||||||
|
| name | VARCHAR(100) | 任务名称 | NOT NULL |
|
||||||
|
| type | VARCHAR(20) | 任务类型:植保/吊运/航拍/巡检/清洗/表演/测绘 | NOT NULL |
|
||||||
|
| province | VARCHAR(20) | 省份 | |
|
||||||
|
| city | VARCHAR(20) | 城市 | |
|
||||||
|
| district | VARCHAR(20) | 区县 | |
|
||||||
|
| address | VARCHAR(200) | 详细地址 | |
|
||||||
|
| latitude | DECIMAL(10,6) | 纬度坐标 | |
|
||||||
|
| longitude | DECIMAL(10,6) | 经度坐标 | |
|
||||||
|
| scene_images | JSON | 作业场景图(file_id数组) | |
|
||||||
|
| required_count | INT | 需求人数 | DEFAULT 1 |
|
||||||
|
| start_time | DATETIME | 开始时间 | NOT NULL |
|
||||||
|
| end_time | DATETIME | 结束时间 | |
|
||||||
|
| reward | DECIMAL(10,2) | 报酬(元) | NOT NULL |
|
||||||
|
| insurance_require | JSON | 保险要求:三者险/飞手险/机身险 | |
|
||||||
|
| contact_name | VARCHAR(50) | 联系人姓名 | |
|
||||||
|
| contact_phone | VARCHAR(20) | 联系电话 | NOT NULL |
|
||||||
|
| remark | TEXT | 备注 | |
|
||||||
|
| status | TINYINT | 状态:0草稿 1待接单 2已接单 3执行中 4已完成 5已取消 | DEFAULT 0 |
|
||||||
|
| is_premium | TINYINT | 是否优质任务:0否 1是 | DEFAULT 0 |
|
||||||
|
| premium_cost | INT | 优质任务积分消耗 | DEFAULT 0 |
|
||||||
|
| publisher_id | BIGINT | 发布人ID(关联user.id) | NOT NULL |
|
||||||
|
| accepted_pilot_id | BIGINT | 接单飞手ID(关联pilot.id) | |
|
||||||
|
| platform_fee | DECIMAL(10,2) | 平台服务费 | DEFAULT 0.00 |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE task (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
task_no VARCHAR(32) NOT NULL COMMENT '任务编号(全局唯一)',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '任务名称',
|
||||||
|
type VARCHAR(20) NOT NULL COMMENT '任务类型:植保/吊运/航拍/巡检/清洗/表演/测绘',
|
||||||
|
province VARCHAR(20) COMMENT '省份',
|
||||||
|
city VARCHAR(20) COMMENT '城市',
|
||||||
|
district VARCHAR(20) COMMENT '区县',
|
||||||
|
address VARCHAR(200) COMMENT '详细地址',
|
||||||
|
latitude DECIMAL(10,6) COMMENT '纬度坐标',
|
||||||
|
longitude DECIMAL(10,6) COMMENT '经度坐标',
|
||||||
|
scene_images JSON COMMENT '作业场景图(file_id数组)',
|
||||||
|
required_count INT DEFAULT 1 COMMENT '需求人数',
|
||||||
|
start_time DATETIME NOT NULL COMMENT '开始时间',
|
||||||
|
end_time DATETIME COMMENT '结束时间',
|
||||||
|
reward DECIMAL(10,2) NOT NULL COMMENT '报酬(元)',
|
||||||
|
insurance_require JSON COMMENT '保险要求:三者险/飞手险/机身险',
|
||||||
|
contact_name VARCHAR(50) COMMENT '联系人姓名',
|
||||||
|
contact_phone VARCHAR(20) NOT NULL COMMENT '联系电话',
|
||||||
|
remark TEXT COMMENT '备注',
|
||||||
|
status TINYINT DEFAULT 0 COMMENT '状态:0草稿 1待接单 2已接单 3执行中 4已完成 5已取消',
|
||||||
|
is_premium TINYINT DEFAULT 0 COMMENT '是否优质任务:0否 1是',
|
||||||
|
premium_cost INT DEFAULT 0 COMMENT '优质任务积分消耗',
|
||||||
|
publisher_id BIGINT NOT NULL COMMENT '发布人ID',
|
||||||
|
accepted_pilot_id BIGINT COMMENT '接单飞手ID',
|
||||||
|
platform_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT '平台服务费',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
UNIQUE KEY uk_task_no (task_no),
|
||||||
|
INDEX idx_type_status (type, status),
|
||||||
|
INDEX idx_city_status (city, status),
|
||||||
|
INDEX idx_publisher (publisher_id),
|
||||||
|
INDEX idx_pilot (accepted_pilot_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.3 商家表 `merchant`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| user_id | BIGINT | 关联用户ID | UNIQUE, NOT NULL |
|
||||||
|
| name | VARCHAR(100) | 商家名称 | NOT NULL |
|
||||||
|
| address | VARCHAR(200) | 商家地址 | |
|
||||||
|
| door_image | BIGINT | 门头照片file_id | |
|
||||||
|
| license_file_id | BIGINT | 营业执照file_id | |
|
||||||
|
| contact_name | VARCHAR(50) | 联系人 | |
|
||||||
|
| contact_phone | VARCHAR(20) | 联系电话 | |
|
||||||
|
| latitude | DECIMAL(10,6) | 纬度 | |
|
||||||
|
| longitude | DECIMAL(10,6) | 经度 | |
|
||||||
|
| service_types | JSON | 服务类型数组 | |
|
||||||
|
| description | TEXT | 商家简介 | |
|
||||||
|
| cert_status | TINYINT | 认证状态:0未认证 1认证中 2已认证 3认证失败 | DEFAULT 0 |
|
||||||
|
| cert_time | DATETIME | 认证时间 | |
|
||||||
|
| rating | DECIMAL(2,1) | 评分(1.0-5.0) | DEFAULT 5.0 |
|
||||||
|
| order_count | INT | 成交订单数 | DEFAULT 0 |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE merchant (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '商家名称',
|
||||||
|
address VARCHAR(200) COMMENT '商家地址',
|
||||||
|
door_image BIGINT COMMENT '门头照片file_id',
|
||||||
|
license_file_id BIGINT COMMENT '营业执照file_id',
|
||||||
|
contact_name VARCHAR(50) COMMENT '联系人',
|
||||||
|
contact_phone VARCHAR(20) COMMENT '联系电话',
|
||||||
|
latitude DECIMAL(10,6) COMMENT '纬度',
|
||||||
|
longitude DECIMAL(10,6) COMMENT '经度',
|
||||||
|
service_types JSON COMMENT '服务类型数组',
|
||||||
|
description TEXT COMMENT '商家简介',
|
||||||
|
cert_status TINYINT DEFAULT 0 COMMENT '认证状态:0未认证 1认证中 2已认证 3认证失败',
|
||||||
|
cert_time DATETIME COMMENT '认证时间',
|
||||||
|
rating DECIMAL(2,1) DEFAULT 5.0 COMMENT '评分(1.0-5.0)',
|
||||||
|
order_count INT DEFAULT 0 COMMENT '成交订单数',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
UNIQUE KEY uk_user (user_id),
|
||||||
|
INDEX idx_cert_status (cert_status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商家表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.4 飞手表 `pilot`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| user_id | BIGINT | 关联用户ID | UNIQUE, NOT NULL |
|
||||||
|
| name | VARCHAR(50) | 姓名(实名) | NOT NULL |
|
||||||
|
| phone | VARCHAR(20) | 手机号 | NOT NULL |
|
||||||
|
| birthday | DATE | 出生年月 | |
|
||||||
|
| age | TINYINT | 年龄(自动计算) | |
|
||||||
|
| avatar_file_id | BIGINT | 头像file_id | |
|
||||||
|
| cert_status | TINYINT | 认证状态:0未认证 1认证中 2已认证 3认证失败 | DEFAULT 0 |
|
||||||
|
| cert_time | DATETIME | 认证时间 | |
|
||||||
|
| license_type | VARCHAR(20) | CAAC执照类型:AOPA/UTC/ASFC | |
|
||||||
|
| license_no | VARCHAR(50) | CAAC执照编号 | |
|
||||||
|
| license_file_id | BIGINT | 执照图片file_id | |
|
||||||
|
| drone_models | JSON | 可操控机型数组 | |
|
||||||
|
| service_cities | JSON | 服务城市数组 | |
|
||||||
|
| bio | VARCHAR(200) | 个人简介 | |
|
||||||
|
| rating | DECIMAL(2,1) | 评分 | DEFAULT 5.0 |
|
||||||
|
| order_count | INT | 完成任务数 | DEFAULT 0 |
|
||||||
|
| insurance_expire | DATE | 飞手险到期日 | |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE pilot (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
|
name VARCHAR(50) NOT NULL COMMENT '姓名(实名)',
|
||||||
|
phone VARCHAR(20) NOT NULL COMMENT '手机号',
|
||||||
|
birthday DATE COMMENT '出生年月',
|
||||||
|
age TINYINT COMMENT '年龄(自动计算)',
|
||||||
|
avatar_file_id BIGINT COMMENT '头像file_id',
|
||||||
|
cert_status TINYINT DEFAULT 0 COMMENT '认证状态:0未认证 1认证中 2已认证 3认证失败',
|
||||||
|
cert_time DATETIME COMMENT '认证时间',
|
||||||
|
license_type VARCHAR(20) COMMENT 'CAAC执照类型:AOPA/UTC/ASFC',
|
||||||
|
license_no VARCHAR(50) COMMENT 'CAAC执照编号',
|
||||||
|
license_file_id BIGINT COMMENT '执照图片file_id',
|
||||||
|
drone_models JSON COMMENT '可操控机型数组',
|
||||||
|
service_cities JSON COMMENT '服务城市数组',
|
||||||
|
bio VARCHAR(200) COMMENT '个人简介',
|
||||||
|
rating DECIMAL(2,1) DEFAULT 5.0 COMMENT '评分',
|
||||||
|
order_count INT DEFAULT 0 COMMENT '完成任务数',
|
||||||
|
insurance_expire DATE COMMENT '飞手险到期日',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
UNIQUE KEY uk_user (user_id),
|
||||||
|
INDEX idx_cert_status (cert_status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='飞手表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.5 题库表 `exam_bank`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| name | VARCHAR(100) | 题库名称 | NOT NULL |
|
||||||
|
| cover_image | BIGINT | 封面图file_id | |
|
||||||
|
| description | TEXT | 题库描述 | |
|
||||||
|
| question_count | INT | 题目数量 | DEFAULT 0 |
|
||||||
|
| difficulty | TINYINT | 难度:1简单 2中等 3困难 | DEFAULT 1 |
|
||||||
|
| status | TINYINT | 状态:0禁用 1启用 | DEFAULT 1 |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE exam_bank (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '题库名称',
|
||||||
|
cover_image BIGINT COMMENT '封面图file_id',
|
||||||
|
description TEXT COMMENT '题库描述',
|
||||||
|
question_count INT DEFAULT 0 COMMENT '题目数量',
|
||||||
|
difficulty TINYINT DEFAULT 1 COMMENT '难度:1简单 2中等 3困难',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='题库表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.6 习题表 `exam_question`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| bank_id | BIGINT | 所属题库ID | NOT NULL |
|
||||||
|
| content | TEXT | 题目内容 | NOT NULL |
|
||||||
|
| options | JSON | 选项数组(含ABCD四选项) | |
|
||||||
|
| question_type | TINYINT | 题型:1单选 2多选 3判断 | NOT NULL |
|
||||||
|
| answer | VARCHAR(10) | 标准答案(如:A 或 AB) | NOT NULL |
|
||||||
|
| explanation | TEXT | 答案解析 | |
|
||||||
|
| difficulty | TINYINT | 难度:1简单 2中等 3困难 | DEFAULT 1 |
|
||||||
|
| score | TINYINT | 分值(单选/多选) | DEFAULT 5 |
|
||||||
|
| sort_order | INT | 排序 | DEFAULT 0 |
|
||||||
|
| used_count | INT | 被使用次数 | DEFAULT 0 |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE exam_question (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
bank_id BIGINT NOT NULL COMMENT '所属题库ID',
|
||||||
|
content TEXT NOT NULL COMMENT '题目内容',
|
||||||
|
options JSON COMMENT '选项数组([{"key":"A","val":"..."},...])',
|
||||||
|
question_type TINYINT NOT NULL COMMENT '题型:1单选 2多选 3判断',
|
||||||
|
answer VARCHAR(10) NOT NULL COMMENT '标准答案(如:A 或 AB)',
|
||||||
|
explanation TEXT COMMENT '答案解析',
|
||||||
|
difficulty TINYINT DEFAULT 1 COMMENT '难度:1简单 2中等 3困难',
|
||||||
|
score TINYINT DEFAULT 5 COMMENT '分值',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
used_count INT DEFAULT 0 COMMENT '被使用次数',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
INDEX idx_bank (bank_id),
|
||||||
|
INDEX idx_type (question_type)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='习题表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.7 课程表 `course`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| name | VARCHAR(100) | 课程名称 | NOT NULL |
|
||||||
|
| tags | JSON | 标签数组(认知启蒙/技能进阶/创新应用/竞赛辅导) | |
|
||||||
|
| category | VARCHAR(20) | 分类 | |
|
||||||
|
| target_age | VARCHAR(20) | 适合年龄 | |
|
||||||
|
| location | VARCHAR(200) | 上课地点 | |
|
||||||
|
| latitude | DECIMAL(10,6) | 纬度 | |
|
||||||
|
| longitude | DECIMAL(10,6) | 经度 | |
|
||||||
|
| description | TEXT | 课程描述 | |
|
||||||
|
| highlights | JSON | 课程亮点(3项) | |
|
||||||
|
| duration_desc | VARCHAR(50) | 课时说明 | |
|
||||||
|
| capacity_min | INT | 最小人数 | DEFAULT 1 |
|
||||||
|
| capacity_max | INT | 最大人数 | DEFAULT 20 |
|
||||||
|
| images | JSON | 课程图片file_id数组 | |
|
||||||
|
| price | DECIMAL(10,2) | 课程价格 | NOT NULL |
|
||||||
|
| merchant_id | BIGINT | 所属商家ID | |
|
||||||
|
| status | TINYINT | 状态:0下架 1上架 2已满员 | DEFAULT 1 |
|
||||||
|
| view_count | INT | 浏览次数 | DEFAULT 0 |
|
||||||
|
| book_count | INT | 已预约人数 | DEFAULT 0 |
|
||||||
|
| rating | DECIMAL(2,1) | 课程评分 | DEFAULT 5.0 |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE course (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '课程名称',
|
||||||
|
tags JSON COMMENT '标签数组(认知启蒙/技能进阶/创新应用/竞赛辅导)',
|
||||||
|
category VARCHAR(20) COMMENT '分类',
|
||||||
|
target_age VARCHAR(20) COMMENT '适合年龄',
|
||||||
|
location VARCHAR(200) COMMENT '上课地点',
|
||||||
|
latitude DECIMAL(10,6) COMMENT '纬度',
|
||||||
|
longitude DECIMAL(10,6) COMMENT '经度',
|
||||||
|
description TEXT COMMENT '课程描述',
|
||||||
|
highlights JSON COMMENT '课程亮点(3项)',
|
||||||
|
duration_desc VARCHAR(50) COMMENT '课时说明',
|
||||||
|
capacity_min INT DEFAULT 1 COMMENT '最小人数',
|
||||||
|
capacity_max INT DEFAULT 20 COMMENT '最大人数',
|
||||||
|
images JSON COMMENT '课程图片file_id数组',
|
||||||
|
price DECIMAL(10,2) NOT NULL COMMENT '课程价格',
|
||||||
|
merchant_id BIGINT COMMENT '所属商家ID',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:0下架 1上架 2已满员',
|
||||||
|
view_count INT DEFAULT 0 COMMENT '浏览次数',
|
||||||
|
book_count INT DEFAULT 0 COMMENT '已预约人数',
|
||||||
|
rating DECIMAL(2,1) DEFAULT 5.0 COMMENT '课程评分',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
INDEX idx_category (category),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_merchant (merchant_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.8 用户表 `user`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| openid | VARCHAR(64) | 微信openId | UNIQUE, NOT NULL |
|
||||||
|
| nickname | VARCHAR(50) | 用户昵称 | |
|
||||||
|
| avatar_url | VARCHAR(500) | 头像URL | |
|
||||||
|
| phone | VARCHAR(20) | 手机号 | |
|
||||||
|
| gender | TINYINT | 性别:0未知 1男 2女 | DEFAULT 0 |
|
||||||
|
| bio | VARCHAR(200) | 个人简介 | |
|
||||||
|
| role_type | TINYINT | 角色类型:1普通用户 2飞手 3商家 4机构 | DEFAULT 1 |
|
||||||
|
| points | INT | 积分余额 | DEFAULT 0 |
|
||||||
|
| is_pilot_bound | TINYINT | 是否已绑定飞手身份 | DEFAULT 0 |
|
||||||
|
| is_merchant_bound | TINYINT | 是否已绑定商家身份 | DEFAULT 0 |
|
||||||
|
| pilot_id | BIGINT | 关联飞手ID | |
|
||||||
|
| merchant_id | BIGINT | 关联商家ID | |
|
||||||
|
| status | TINYINT | 账号状态:0禁用 1正常 | DEFAULT 1 |
|
||||||
|
| last_login_at | DATETIME | 最后登录时间 | |
|
||||||
|
| created_at | DATETIME | 注册时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
openid VARCHAR(64) NOT NULL COMMENT '微信openId',
|
||||||
|
nickname VARCHAR(50) COMMENT '用户昵称',
|
||||||
|
avatar_url VARCHAR(500) COMMENT '头像URL',
|
||||||
|
phone VARCHAR(20) COMMENT '手机号',
|
||||||
|
gender TINYINT DEFAULT 0 COMMENT '性别:0未知 1男 2女',
|
||||||
|
bio VARCHAR(200) COMMENT '个人简介',
|
||||||
|
role_type TINYINT DEFAULT 1 COMMENT '角色类型:1普通用户 2飞手 3商家 4机构',
|
||||||
|
points INT DEFAULT 0 COMMENT '积分余额',
|
||||||
|
is_pilot_bound TINYINT DEFAULT 0 COMMENT '是否已绑定飞手身份',
|
||||||
|
is_merchant_bound TINYINT DEFAULT 0 COMMENT '是否已绑定商家身份',
|
||||||
|
pilot_id BIGINT COMMENT '关联飞手ID',
|
||||||
|
merchant_id BIGINT COMMENT '关联商家ID',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '账号状态:0禁用 1正常',
|
||||||
|
last_login_at DATETIME COMMENT '最后登录时间',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
UNIQUE KEY uk_openid (openid),
|
||||||
|
INDEX idx_phone (phone)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.9 任务执行记录表 `task_record`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| task_id | BIGINT | 任务ID | NOT NULL |
|
||||||
|
| user_id | BIGINT | 关联用户ID | NOT NULL |
|
||||||
|
| pilot_id | BIGINT | 接单飞手ID | |
|
||||||
|
| merchant_id | BIGINT | 发单商家ID | |
|
||||||
|
| status | TINYINT | 状态:1待接单 2已接单 3执行中 4已完成 5已取消 | DEFAULT 1 |
|
||||||
|
| apply_time | DATETIME | 申请/接单时间 | |
|
||||||
|
| accept_time | DATETIME | 飞手接单时间 | |
|
||||||
|
| start_time | DATETIME | 实际开始时间 | |
|
||||||
|
| finish_time | DATETIME | 完成时间 | |
|
||||||
|
| cancel_time | DATETIME | 取消时间 | |
|
||||||
|
| cancel_reason | VARCHAR(200) | 取消原因 | |
|
||||||
|
| insurance_verified | TINYINT | 保险是否校验:0否 1是 | DEFAULT 0 |
|
||||||
|
| actual_reward | DECIMAL(10,2) | 实际报酬 | |
|
||||||
|
| platform_fee | DECIMAL(10,2) | 平台服务费 | |
|
||||||
|
| rating | TINYINT | 用户评分(1-5) | |
|
||||||
|
| rating_comment | VARCHAR(200) | 评价内容 | |
|
||||||
|
| created_at | DATETIME | 创建时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE task_record (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
task_id BIGINT NOT NULL COMMENT '任务ID',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '关联用户ID',
|
||||||
|
pilot_id BIGINT COMMENT '接单飞手ID',
|
||||||
|
merchant_id BIGINT COMMENT '发单商家ID',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:1待接单 2已接单 3执行中 4已完成 5已取消',
|
||||||
|
apply_time DATETIME COMMENT '申请/接单时间',
|
||||||
|
accept_time DATETIME COMMENT '飞手接单时间',
|
||||||
|
start_time DATETIME COMMENT '实际开始时间',
|
||||||
|
finish_time DATETIME COMMENT '完成时间',
|
||||||
|
cancel_time DATETIME COMMENT '取消时间',
|
||||||
|
cancel_reason VARCHAR(200) COMMENT '取消原因',
|
||||||
|
insurance_verified TINYINT DEFAULT 0 COMMENT '保险是否校验:0否 1是',
|
||||||
|
actual_reward DECIMAL(10,2) COMMENT '实际报酬',
|
||||||
|
platform_fee DECIMAL(10,2) COMMENT '平台服务费',
|
||||||
|
rating TINYINT COMMENT '用户评分(1-5)',
|
||||||
|
rating_comment VARCHAR(200) COMMENT '评价内容',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
UNIQUE KEY uk_task_user (task_id, user_id),
|
||||||
|
INDEX idx_user_status (user_id, status),
|
||||||
|
INDEX idx_pilot_status (pilot_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务执行记录表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.10 文件表 `file`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| file_name | VARCHAR(200) | 原始文件名 | NOT NULL |
|
||||||
|
| file_key | VARCHAR(200) | 存储路径/OSS Key | NOT NULL |
|
||||||
|
| file_suffix | VARCHAR(10) | 文件后缀 | |
|
||||||
|
| file_url | VARCHAR(500) | 访问URL | |
|
||||||
|
| file_size | BIGINT | 文件大小(字节) | |
|
||||||
|
| file_type | VARCHAR(20) | 类型:营业执照/图片/飞手执照/其他 | |
|
||||||
|
| uploader_id | BIGINT | 上传用户ID | |
|
||||||
|
| status | TINYINT | 状态:0禁用 1正常 | DEFAULT 1 |
|
||||||
|
| created_at | DATETIME | 上传时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `file` (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
file_name VARCHAR(200) NOT NULL COMMENT '原始文件名',
|
||||||
|
file_key VARCHAR(200) NOT NULL COMMENT '存储路径/OSS Key',
|
||||||
|
file_suffix VARCHAR(10) COMMENT '文件后缀',
|
||||||
|
file_url VARCHAR(500) COMMENT '访问URL',
|
||||||
|
file_size BIGINT COMMENT '文件大小(字节)',
|
||||||
|
file_type VARCHAR(20) COMMENT '类型:营业执照/图片/飞手执照/其他',
|
||||||
|
uploader_id BIGINT COMMENT '上传用户ID',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:0禁用 1正常',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
INDEX idx_type (file_type),
|
||||||
|
INDEX idx_uploader (uploader_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.11 课程预约表 `course_appointment`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 | 约束 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | BIGINT | 主键 | PK, AUTO_INCREMENT |
|
||||||
|
| user_id | BIGINT | 用户ID | NOT NULL |
|
||||||
|
| course_id | BIGINT | 课程ID | NOT NULL |
|
||||||
|
| merchant_id | BIGINT | 商家ID | |
|
||||||
|
| appointment_date | DATETIME | 预约时间 | NOT NULL |
|
||||||
|
| person_count | INT | 预约人数 | DEFAULT 1 |
|
||||||
|
| contact_name | VARCHAR(50) | 联系人姓名 | |
|
||||||
|
| contact_phone | VARCHAR(20) | 联系人电话 | |
|
||||||
|
| remark | VARCHAR(200) | 备注 | |
|
||||||
|
| status | TINYINT | 状态:1已预约 2已确认 3已完成 4已取消 | DEFAULT 1 |
|
||||||
|
| confirm_time | DATETIME | 商家确认时间 | |
|
||||||
|
| cancel_time | DATETIME | 取消时间 | |
|
||||||
|
| cancel_reason | VARCHAR(200) | 取消原因 | |
|
||||||
|
| amount | DECIMAL(10,2) | 支付金额 | DEFAULT 0.00 |
|
||||||
|
| pay_status | TINYINT | 支付状态:0未支付 1已支付 2已退款 | DEFAULT 0 |
|
||||||
|
| created_at | DATETIME | 预约时间 | DEFAULT CURRENT_TIMESTAMP |
|
||||||
|
| updated_at | DATETIME | 更新时间 | |
|
||||||
|
| deleted_at | DATETIME | 软删除时间 | |
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE course_appointment (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||||
|
course_id BIGINT NOT NULL COMMENT '课程ID',
|
||||||
|
merchant_id BIGINT COMMENT '商家ID',
|
||||||
|
appointment_date DATETIME NOT NULL COMMENT '预约时间',
|
||||||
|
person_count INT DEFAULT 1 COMMENT '预约人数',
|
||||||
|
contact_name VARCHAR(50) COMMENT '联系人姓名',
|
||||||
|
contact_phone VARCHAR(20) COMMENT '联系人电话',
|
||||||
|
remark VARCHAR(200) COMMENT '备注',
|
||||||
|
status TINYINT DEFAULT 1 COMMENT '状态:1已预约 2已确认 3已完成 4已取消',
|
||||||
|
confirm_time DATETIME COMMENT '商家确认时间',
|
||||||
|
cancel_time DATETIME COMMENT '取消时间',
|
||||||
|
cancel_reason VARCHAR(200) COMMENT '取消原因',
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '支付金额',
|
||||||
|
pay_status TINYINT DEFAULT 0 COMMENT '支付状态:0未支付 1已支付 2已退款',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '预约时间',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted_at DATETIME DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
UNIQUE KEY uk_user_course_date (user_id, course_id, appointment_date),
|
||||||
|
INDEX idx_course_status (course_id, status),
|
||||||
|
INDEX idx_merchant (merchant_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程预约表';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11.12 表间关系(ER)
|
||||||
|
|
||||||
|
```
|
||||||
|
user ─────┬── 1:1 pilot(飞手身份)
|
||||||
|
└── 1:1 merchant(商家身份)
|
||||||
|
└── 1:N task_record(我的任务)
|
||||||
|
└── 1:N course_appointment(课程预约)
|
||||||
|
|
||||||
|
pilot ────── N:1 task_record(接单飞手)
|
||||||
|
merchant ─── N:1 task_record(发单商家)
|
||||||
|
N:1 course(商家开课)
|
||||||
|
|
||||||
|
task ──────── 1:1 task_record(任务→执行记录)
|
||||||
|
course ────── 1:N course_appointment(课程→预约)
|
||||||
|
|
||||||
|
file ──────── N:1 user(上传者)
|
||||||
|
N:1 pilot(执照图片)
|
||||||
|
N:1 merchant(营业执照)
|
||||||
|
N:1 task(作业场景图)
|
||||||
|
N:1 course(课程图片)
|
||||||
|
|
||||||
|
exam_bank ── 1:N exam_question(题库→习题)
|
||||||
|
```
|
||||||
30
mvn17
Executable file
30
mvn17
Executable file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# 翼云Hub 构建包装器
|
||||||
|
# 使用 Homebrew OpenJDK 17 运行 Maven,不影响系统默认 Java 8 环境
|
||||||
|
#
|
||||||
|
# 常用命令:
|
||||||
|
# ./mvn17 compile 编译
|
||||||
|
# ./mvn17 clean package 打包
|
||||||
|
# ./mvn17 spring-boot:run 启动服务(开发模式)
|
||||||
|
# ./mvn17 test 运行测试
|
||||||
|
# ./mvn17 run 运行已打包的 jar
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
JAVA17_HOME="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
export JAVA_HOME="$JAVA17_HOME"
|
||||||
|
|
||||||
|
# run 子命令:直接用 Java 17 运行已打包的 jar
|
||||||
|
if [[ "$1" == "run" ]]; then
|
||||||
|
JAR=$(find "$SCRIPT_DIR/target" -name "laop-admin-*.jar" ! -name "*.original" 2>/dev/null | head -1)
|
||||||
|
if [[ -z "$JAR" ]]; then
|
||||||
|
echo "未找到 jar,请先执行:./mvn17 clean package" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec "$JAVA17_HOME/bin/java" -jar "$JAR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 其他命令透传给 Maven Wrapper
|
||||||
|
exec "$SCRIPT_DIR/mvnw" "$@"
|
||||||
295
mvnw
vendored
Executable file
295
mvnw
vendored
Executable file
|
|
@ -0,0 +1,295 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptDir="$(dirname "$0")"
|
||||||
|
scriptName="$(basename "$0")"
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
actualDistributionDir=""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||||
|
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$distributionUrlNameMain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
# enable globbing to iterate over items
|
||||||
|
set +f
|
||||||
|
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$(basename "$dir")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||||
|
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||||
|
die "Could not find Maven distribution directory in extracted archive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
189
mvnw.cmd
vendored
Normal file
189
mvnw.cmd
vendored
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
|
||||||
|
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||||
|
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_WRAPPER_DISTS = $null
|
||||||
|
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||||
|
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||||
|
} else {
|
||||||
|
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
$actualDistributionDir = ""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||||
|
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||||
|
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||||
|
$actualDistributionDir = $distributionUrlNameMain
|
||||||
|
}
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||||
|
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||||
|
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||||
|
$actualDistributionDir = $_.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
125
pom.xml
Normal file
125
pom.xml
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<?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>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.12</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.haisen</groupId>
|
||||||
|
<artifactId>laop-admin</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>laop-admin</name>
|
||||||
|
<description>翼云Hub · 无人机信息开放平台</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
<postgresql.version>42.7.3</postgresql.version>
|
||||||
|
<redisson.version>3.27.2</redisson.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Web -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 参数校验 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JPA:Spring Boot 3.2 自带 Hibernate 6.4,原生支持 PG jsonb(@JdbcTypeCode(SqlTypes.JSON)) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- PostgreSQL 驱动:≥42.6.0 才完整支持 jsonb JDBC 绑定 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>${postgresql.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis:Spring Data Redis 自动配置元数据 + Redisson 客户端 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.redisson</groupId>
|
||||||
|
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||||
|
<version>${redisson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Flyway:Spring Boot 3.2 自带 9.22.x,无需显式声明版本 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 工具 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>${project.artifactId}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<!-- 开发环境 -->
|
||||||
|
<profile>
|
||||||
|
<id>dev</id>
|
||||||
|
<properties>
|
||||||
|
<spring.profiles.active>dev</spring.profiles.active>
|
||||||
|
</properties>
|
||||||
|
<activation>
|
||||||
|
<activeByDefault>true</activeByDefault>
|
||||||
|
</activation>
|
||||||
|
</profile>
|
||||||
|
<!-- 生产环境 -->
|
||||||
|
<profile>
|
||||||
|
<id>prod</id>
|
||||||
|
<properties>
|
||||||
|
<spring.profiles.active>prod</spring.profiles.active>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
</project>
|
||||||
15
src/main/java/com/haisen/laop/LaopAdminApplication.java
Normal file
15
src/main/java/com/haisen/laop/LaopAdminApplication.java
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.haisen.laop;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后端服务启动入口。
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class LaopAdminApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(LaopAdminApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
204
src/main/java/com/haisen/laop/client/wx/AbstractWxApiClient.java
Normal file
204
src/main/java/com/haisen/laop/client/wx/AbstractWxApiClient.java
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
package com.haisen.laop.client.wx;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.redisson.api.RBucket;
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信三方 Client 抽象基类:统一承载 token 获取(Redis 缓存)与 HTTP 调用。
|
||||||
|
*
|
||||||
|
* <p><b>token 缓存策略(cache-aside):</b>access_token 非一次性凭据(有效期 7200 秒、
|
||||||
|
* 每日获取次数有限),必须缓存复用 —— 业务触发时先查 Redis,命中直接返回;
|
||||||
|
* 未命中(或强制刷新)才请求微信并回填 Redis,TTL = 微信返回的有效期(expires_in),
|
||||||
|
* 保证过期后自动失效重新获取。Redis 缓存使多实例部署共享同一 token,
|
||||||
|
* 避免各实例各自刷新导致微信侧 token 互相失效。</p>
|
||||||
|
*
|
||||||
|
* <p><b>域名配置化:</b>所有接口 URL = {@code wechat.base-url}(配置注入)+
|
||||||
|
* {@link WxApiPaths} 中的接口路径,Client 内不出现裸域名。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AbstractWxApiClient {
|
||||||
|
|
||||||
|
/** token 失效类错误码:invalid credential,需强制刷新后重试 */
|
||||||
|
public static final int ERR_INVALID_TOKEN = 40001;
|
||||||
|
|
||||||
|
/** token 过期类错误码:access_token expired,需强制刷新后重试 */
|
||||||
|
public static final int ERR_TOKEN_EXPIRED = 42001;
|
||||||
|
|
||||||
|
/** 微信返回错误码字段 */
|
||||||
|
protected static final String WX_FIELD_ERRCODE = "errcode";
|
||||||
|
/** 微信返回错误信息字段 */
|
||||||
|
protected static final String WX_FIELD_ERRMSG = "errmsg";
|
||||||
|
|
||||||
|
/** 单次微信调用超时(秒) */
|
||||||
|
private static final int HTTP_TIMEOUT_SECONDS = 10;
|
||||||
|
|
||||||
|
/** Redis token 缓存 key 前缀与后缀,中段由子类提供(如 mp / mini)区分两端应用 */
|
||||||
|
private static final String TOKEN_KEY_PREFIX = "laop:wx:";
|
||||||
|
private static final String TOKEN_KEY_SUFFIX = ":access_token";
|
||||||
|
|
||||||
|
/** 微信开放平台 API 域名(配置化,默认官方域名) */
|
||||||
|
@Value("${wechat.base-url:https://api.weixin.qq.com}")
|
||||||
|
protected String baseUrl;
|
||||||
|
|
||||||
|
protected final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private final HttpClient httpClient = HttpClient.newBuilder().build();
|
||||||
|
private final RedissonClient redissonClient;
|
||||||
|
|
||||||
|
protected AbstractWxApiClient(ObjectMapper objectMapper, RedissonClient redissonClient) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.redissonClient = redissonClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis token 缓存 key 的业务段(如 {@code mp} / {@code mini}),区分不同微信应用。
|
||||||
|
*/
|
||||||
|
protected abstract String tokenCacheSegment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属微信应用的 appId(由子类从配置注入)。
|
||||||
|
*/
|
||||||
|
protected abstract String appId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所属微信应用的 appSecret(由子类从配置注入)。
|
||||||
|
*/
|
||||||
|
protected abstract String appSecret();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志标识(如 "微信服务号" / "微信小程序"),统一日志前缀便于排查。
|
||||||
|
*/
|
||||||
|
protected abstract String logTag();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断该端微信能力是否已配置(appId/appSecret 齐全)。
|
||||||
|
*
|
||||||
|
* <p>未配置时调用方应降级处理(跳过发送/提示配置缺失),不阻断主业务。</p>
|
||||||
|
*/
|
||||||
|
public boolean isConfigured() {
|
||||||
|
return StringUtils.isNotBlank(appId()) && StringUtils.isNotBlank(appSecret());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 access_token(cache-aside:先 Redis 后微信,结果回填 Redis)。
|
||||||
|
*
|
||||||
|
* @param forceRefresh true 强制刷新(token 失效重试场景),跳过缓存直取微信
|
||||||
|
* @return access_token;获取失败返回 null,调用方降级处理
|
||||||
|
*/
|
||||||
|
public synchronized String getAccessToken(boolean forceRefresh) {
|
||||||
|
RBucket<String> bucket = redissonClient.getBucket(tokenCacheKey());
|
||||||
|
if (!forceRefresh) {
|
||||||
|
String cached = bucket.get();
|
||||||
|
if (StringUtils.isNotBlank(cached)) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fetchAndCacheToken(bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求微信获取新 token 并回填 Redis(TTL = 微信返回的 expires_in 有效期)。
|
||||||
|
*/
|
||||||
|
private String fetchAndCacheToken(RBucket<String> bucket) {
|
||||||
|
String url = baseUrl + String.format(WxApiPaths.ACCESS_TOKEN,
|
||||||
|
URLEncoder.encode(appId(), StandardCharsets.UTF_8),
|
||||||
|
URLEncoder.encode(appSecret(), StandardCharsets.UTF_8));
|
||||||
|
try {
|
||||||
|
JsonNode json = execute(getRequest(URI.create(url)));
|
||||||
|
int errcode = json.path(WX_FIELD_ERRCODE).asInt(0);
|
||||||
|
if (errcode != 0) {
|
||||||
|
log.error("[{}] 获取 access_token 失败: code={}, msg={}",
|
||||||
|
logTag(), errcode, json.path(WX_FIELD_ERRMSG).asText());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String token = json.path("access_token").asText("");
|
||||||
|
if (token.isBlank()) {
|
||||||
|
log.error("[{}] 获取 access_token 返回为空", logTag());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// TTL 严格等于微信下发的有效期(秒),过期自动重新获取;
|
||||||
|
// 微信侧 token 过期后仍有短暂宽限期,无需额外预留余量
|
||||||
|
long expiresInSeconds = json.path("expires_in").asLong(7200);
|
||||||
|
bucket.set(token, Duration.ofSeconds(expiresInSeconds));
|
||||||
|
log.info("[{}] access_token 已刷新并写入 Redis, ttl={}s", logTag(), expiresInSeconds);
|
||||||
|
return token;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.error("[{}] 获取 access_token 被中断", logTag());
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("[{}] 获取 access_token 网络异常", logTag(), e);
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[{}] 获取 access_token 异常", logTag(), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token 在 Redis 中的完整缓存 key。
|
||||||
|
*/
|
||||||
|
private String tokenCacheKey() {
|
||||||
|
return TOKEN_KEY_PREFIX + tokenCacheSegment() + TOKEN_KEY_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 通用 HTTP 执行(子类按接口语义组合使用) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 GET 请求。
|
||||||
|
*/
|
||||||
|
protected HttpRequest getRequest(URI uri) {
|
||||||
|
return HttpRequest.newBuilder()
|
||||||
|
.uri(uri)
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofSeconds(HTTP_TIMEOUT_SECONDS))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 POST JSON 请求。
|
||||||
|
*/
|
||||||
|
protected HttpRequest postJsonRequest(URI uri, Object body) throws IOException {
|
||||||
|
return HttpRequest.newBuilder()
|
||||||
|
.uri(uri)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
|
||||||
|
.timeout(Duration.ofSeconds(HTTP_TIMEOUT_SECONDS))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行请求并解析响应为 JSON(微信 JSON 接口通用)。
|
||||||
|
*/
|
||||||
|
protected JsonNode execute(HttpRequest request) throws IOException, InterruptedException {
|
||||||
|
HttpResponse<String> response =
|
||||||
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
return objectMapper.readTree(response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行请求并返回原始字节数组(微信二进制接口通用,如小程序码图片)。
|
||||||
|
*
|
||||||
|
* <p>微信二进制接口失败时返回 JSON 报文,调用方需按响应 Content-Type 区分处理。</p>
|
||||||
|
*/
|
||||||
|
protected HttpResponse<byte[]> executeForBytes(HttpRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
return httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/java/com/haisen/laop/client/wx/WxApiPaths.java
Normal file
36
src/main/java/com/haisen/laop/client/wx/WxApiPaths.java
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.haisen.laop.client.wx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信开放平台接口路径常量(仅存接口名称/路径,域名统一由 {@code wechat.base-url} 配置注入)。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>与微信三方交互的 URL 收口此处,禁止在业务代码中裸写接口地址;
|
||||||
|
* 路径中的 {@code %s} 为请求参数占位符,由各 Client 按参数顺序格式化填充。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class WxApiPaths {
|
||||||
|
|
||||||
|
/** 获取全局接口调用凭据 access_token(服务号/小程序通用,参数:appId、appSecret)。 */
|
||||||
|
public static final String ACCESS_TOKEN =
|
||||||
|
"/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
|
||||||
|
|
||||||
|
/** 服务号:获取用户信息(参数:accessToken、openid),返回 subscribe/unionid 等字段。 */
|
||||||
|
public static final String MP_USER_INFO =
|
||||||
|
"/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
|
||||||
|
|
||||||
|
/** 服务号:发送模板消息(参数:accessToken)。 */
|
||||||
|
public static final String MP_TEMPLATE_SEND =
|
||||||
|
"/cgi-bin/message/template/send?access_token=%s";
|
||||||
|
|
||||||
|
/** 小程序:code 换登录会话(参数:appId、appSecret、jsCode)。 */
|
||||||
|
public static final String MINI_CODE2SESSION =
|
||||||
|
"/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
|
||||||
|
|
||||||
|
/** 小程序:获取不限制数量小程序码(参数:accessToken)。 */
|
||||||
|
public static final String MINI_WXACODE_UNLIMIT =
|
||||||
|
"/wxa/getwxacodeunlimit?access_token=%s";
|
||||||
|
|
||||||
|
private WxApiPaths() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/main/java/com/haisen/laop/client/wx/WxMiniClient.java
Normal file
173
src/main/java/com/haisen/laop/client/wx/WxMiniClient.java
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
package com.haisen.laop.client.wx;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序三方 Client:code 换登录会话、小程序码生成。
|
||||||
|
*
|
||||||
|
* <p>仅封装微信接口协议(HTTP 组装/解析),不含业务规则;
|
||||||
|
* token 的 Redis 缓存逻辑统一在 {@link AbstractWxApiClient}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class WxMiniClient extends AbstractWxApiClient {
|
||||||
|
|
||||||
|
/** 微信返回 openid 字段 */
|
||||||
|
private static final String WX_FIELD_OPENID = "openid";
|
||||||
|
/** 微信返回 unionid 字段 */
|
||||||
|
private static final String WX_FIELD_UNIONID = "unionid";
|
||||||
|
|
||||||
|
/** 小程序码落地页由业务方传入,check_path 固定关闭:小程序未发布版本跳过页面存在性校验 */
|
||||||
|
private static final boolean WXACODE_CHECK_PATH = false;
|
||||||
|
|
||||||
|
@Value("${wechat.mini.app-id}")
|
||||||
|
private String appId;
|
||||||
|
|
||||||
|
@Value("${wechat.mini.app-secret}")
|
||||||
|
private String appSecret;
|
||||||
|
|
||||||
|
public WxMiniClient(ObjectMapper objectMapper, RedissonClient redissonClient) {
|
||||||
|
super(objectMapper, redissonClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String tokenCacheSegment() {
|
||||||
|
return "mini";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String appId() {
|
||||||
|
return appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String appSecret() {
|
||||||
|
return appSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String logTag() {
|
||||||
|
return "微信小程序";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* code2session 会话凭证:openid 为登录主键,unionid 用于跨端(服务号)身份关联,可能为 null。
|
||||||
|
*/
|
||||||
|
public record WxSession(String openid, String unionid) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用微信 code 换登录会话(openid + unionid)。
|
||||||
|
*
|
||||||
|
* <p>登录是同步强依赖链路,失败直接抛业务异常终止登录流程。</p>
|
||||||
|
*
|
||||||
|
* @param code wx.login() 临时登录凭证
|
||||||
|
* @return 会话凭证
|
||||||
|
* @throws BusinessException 微信返回错误或 openid 为空时抛出
|
||||||
|
*/
|
||||||
|
public WxSession jscode2session(String code) {
|
||||||
|
// 防呆:appId/appSecret 仍是脚手架占位值时给出明确配置指引
|
||||||
|
if (appId.contains("your-appid") || appSecret.contains("your-secret")) {
|
||||||
|
throw new BusinessException(500, "后端未配置微信小程序 appId/appSecret,"
|
||||||
|
+ "请在 application-dev.yml 中填入或设置环境变量 WECHAT_MINI_APP_ID/SECRET");
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = baseUrl + String.format(WxApiPaths.MINI_CODE2SESSION,
|
||||||
|
URLEncoder.encode(appId, StandardCharsets.UTF_8),
|
||||||
|
URLEncoder.encode(appSecret, StandardCharsets.UTF_8),
|
||||||
|
URLEncoder.encode(code, StandardCharsets.UTF_8));
|
||||||
|
try {
|
||||||
|
JsonNode json = execute(getRequest(URI.create(url)));
|
||||||
|
if (json.has(WX_FIELD_ERRCODE)) {
|
||||||
|
String errMsg = json.path(WX_FIELD_ERRMSG).asText("未知错误");
|
||||||
|
log.error("[微信小程序] code2session 失败: code={}, msg={}",
|
||||||
|
json.path(WX_FIELD_ERRCODE).asText(), errMsg);
|
||||||
|
throw new BusinessException(500, "微信登录失败:" + errMsg);
|
||||||
|
}
|
||||||
|
String openid = json.path(WX_FIELD_OPENID).asText("");
|
||||||
|
if (openid.isBlank()) {
|
||||||
|
throw new BusinessException(500, "微信返回 openid 为空");
|
||||||
|
}
|
||||||
|
// unionid 仅在开放平台绑定后返回,未绑定时为空,不影响登录
|
||||||
|
String unionid = json.path(WX_FIELD_UNIONID).asText("");
|
||||||
|
// TODO 临时调试日志:排查开放平台绑定后 unionid 未返回问题,验证后立即移除
|
||||||
|
log.info("[临时调试] code2session 原始响应: {}", json);
|
||||||
|
return new WxSession(openid, unionid.isBlank() ? null : unionid);
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new BusinessException(500, "微信登录超时,请稍后重试");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("[微信小程序] code2session 调用异常", e);
|
||||||
|
throw new BusinessException(500, "微信登录网络异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成不限制数量小程序码(scene 携带业务参数,如推广人 ID)。
|
||||||
|
*
|
||||||
|
* <p>用户可见的同步动作,失败抛业务异常由前端提示。</p>
|
||||||
|
*
|
||||||
|
* @param page 落地页路径(如 pages/home/index)
|
||||||
|
* @param scene scene 参数(如 i=123)
|
||||||
|
* @param envVersion 小程序码打开版本:release / trial / develop;空则微信默认 release
|
||||||
|
* @return 小程序码图片二进制
|
||||||
|
* @throws BusinessException 微信返回错误或图片为空时抛出
|
||||||
|
*/
|
||||||
|
public byte[] getWxacodeUnlimit(String page, String scene, String envVersion) {
|
||||||
|
String token = getAccessToken(false);
|
||||||
|
if (token == null) {
|
||||||
|
throw new BusinessException(500, "获取微信接口凭据失败,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("page", page);
|
||||||
|
body.put("scene", scene);
|
||||||
|
body.put("check_path", WXACODE_CHECK_PATH);
|
||||||
|
if (envVersion != null && !envVersion.isBlank()) {
|
||||||
|
body.put("env_version", envVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = baseUrl + String.format(WxApiPaths.MINI_WXACODE_UNLIMIT, token);
|
||||||
|
try {
|
||||||
|
var response = executeForBytes(postJsonRequest(URI.create(url), body));
|
||||||
|
// 微信失败时返回 JSON(application/json),成功时直接返回图片二进制流
|
||||||
|
String contentType = response.headers().firstValue("Content-Type").orElse("");
|
||||||
|
if (contentType.contains("application/json")) {
|
||||||
|
JsonNode json = objectMapper.readTree(new String(response.body(), StandardCharsets.UTF_8));
|
||||||
|
log.error("[微信小程序] 生成小程序码失败: code={}, msg={}",
|
||||||
|
json.path(WX_FIELD_ERRCODE).asInt(), json.path(WX_FIELD_ERRMSG).asText());
|
||||||
|
throw new BusinessException(500, "生成小程序码失败,请稍后重试");
|
||||||
|
}
|
||||||
|
byte[] imageBytes = response.body();
|
||||||
|
if (imageBytes == null || imageBytes.length == 0) {
|
||||||
|
throw new BusinessException(500, "生成小程序码失败,请稍后重试");
|
||||||
|
}
|
||||||
|
return imageBytes;
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new BusinessException(500, "生成小程序码超时,请稍后重试");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("[微信小程序] 生成小程序码调用异常", e);
|
||||||
|
throw new BusinessException(500, "生成小程序码网络异常,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/main/java/com/haisen/laop/client/wx/WxMpClient.java
Normal file
165
src/main/java/com/haisen/laop/client/wx/WxMpClient.java
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
package com.haisen.laop.client.wx;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信服务号(公众号 MP)三方 Client:token 获取、用户信息查询、模板消息发送。
|
||||||
|
*
|
||||||
|
* <p>仅封装微信接口协议(HTTP 组装/解析),不含业务规则;
|
||||||
|
* token 的 Redis 缓存逻辑统一在 {@link AbstractWxApiClient}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class WxMpClient extends AbstractWxApiClient {
|
||||||
|
|
||||||
|
/** 模板 data 结构中每个字段的包裹键({"xxx": {"value": "..."}},微信接口协议要求) */
|
||||||
|
private static final String TMPL_FIELD_VALUE_KEY = "value";
|
||||||
|
|
||||||
|
/** 微信返回 msg_id 字段 */
|
||||||
|
private static final String WX_FIELD_MSG_ID = "msg_id";
|
||||||
|
|
||||||
|
@Value("${wechat.mp.app-id}")
|
||||||
|
private String appId;
|
||||||
|
|
||||||
|
@Value("${wechat.mp.app-secret}")
|
||||||
|
private String appSecret;
|
||||||
|
|
||||||
|
public WxMpClient(ObjectMapper objectMapper, RedissonClient redissonClient) {
|
||||||
|
super(objectMapper, redissonClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String tokenCacheSegment() {
|
||||||
|
return "mp";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String appId() {
|
||||||
|
return appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String appSecret() {
|
||||||
|
return appSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String logTag() {
|
||||||
|
return "微信服务号";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务号用户信息快照(/cgi-bin/user/info 返回的本平台关注字段)。
|
||||||
|
*
|
||||||
|
* @param unionid 用户 unionid(服务号已绑定微信开放平台时返回)
|
||||||
|
* @param subscribe 是否关注服务号:1 已关注,0 未关注
|
||||||
|
*/
|
||||||
|
public record WxMpUserInfo(String unionid, Integer subscribe) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询服务号用户信息(关注状态 + unionid)。
|
||||||
|
*
|
||||||
|
* <p>查询失败返回 empty,调用方据此降级(如放弃自动绑定/视为已关注避免误打扰)。</p>
|
||||||
|
*
|
||||||
|
* @param openid 服务号用户 openid
|
||||||
|
* @return 用户信息快照
|
||||||
|
*/
|
||||||
|
public Optional<WxMpUserInfo> getUserInfo(String openid) {
|
||||||
|
String token = getAccessToken(false);
|
||||||
|
if (token == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String url = baseUrl + String.format(WxApiPaths.MP_USER_INFO, token,
|
||||||
|
URLEncoder.encode(openid, StandardCharsets.UTF_8));
|
||||||
|
try {
|
||||||
|
JsonNode json = execute(getRequest(URI.create(url)));
|
||||||
|
int errcode = json.path(WX_FIELD_ERRCODE).asInt(0);
|
||||||
|
if (errcode != 0) {
|
||||||
|
log.warn("[微信服务号] 查询用户信息失败: openid={}, code={}, msg={}",
|
||||||
|
openid, errcode, json.path(WX_FIELD_ERRMSG).asText());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String unionid = json.path("unionid").asText("");
|
||||||
|
return Optional.of(new WxMpUserInfo(
|
||||||
|
unionid.isBlank() ? null : unionid,
|
||||||
|
json.path("subscribe").asInt(0)));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[微信服务号] 查询用户信息网络异常: openid={}", openid, e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送模板消息(提交微信,送达结果由微信异步回调确认)。
|
||||||
|
*
|
||||||
|
* @param accessToken 调用方先取好的 access_token(token 失效重试策略由调用方掌控)
|
||||||
|
* @param openId 接收人服务号 openid
|
||||||
|
* @param templateId 模板 ID(服务号后台申请获得)
|
||||||
|
* @param data 模板参数(字段 key → 值)
|
||||||
|
* @return 提交结果(errcode=0 表示微信受理成功,含 msgId)
|
||||||
|
*/
|
||||||
|
public WxTemplateSendResult sendTemplate(String accessToken, String openId,
|
||||||
|
String templateId, Map<String, String> data) {
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("touser", openId);
|
||||||
|
body.put("template_id", templateId);
|
||||||
|
body.put("data", wrapTemplateData(data));
|
||||||
|
|
||||||
|
String url = baseUrl + String.format(WxApiPaths.MP_TEMPLATE_SEND, accessToken);
|
||||||
|
try {
|
||||||
|
JsonNode json = execute(postJsonRequest(URI.create(url), body));
|
||||||
|
int errcode = json.path(WX_FIELD_ERRCODE).asInt(0);
|
||||||
|
return new WxTemplateSendResult(
|
||||||
|
errcode == 0 ? json.path(WX_FIELD_MSG_ID).asLong(0L) : 0L,
|
||||||
|
errcode,
|
||||||
|
json.path(WX_FIELD_ERRMSG).asText("未知错误"));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return new WxTemplateSendResult(0L, -1, "发送被中断");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("[微信服务号] 发送模板消息网络异常: openId={}", openId, e);
|
||||||
|
return new WxTemplateSendResult(0L, -1, "微信接口网络异常:" + e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[微信服务号] 发送模板消息异常: openId={}", openId, e);
|
||||||
|
return new WxTemplateSendResult(0L, -1, "微信接口调用异常:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板消息提交结果。
|
||||||
|
*
|
||||||
|
* @param msgId 微信消息 ID(受理成功时返回,送达回调按此关联)
|
||||||
|
* @param errcode 错误码:0 成功,-1 本地异常(网络/中断)
|
||||||
|
* @param errmsg 错误信息
|
||||||
|
*/
|
||||||
|
public record WxTemplateSendResult(long msgId, int errcode, String errmsg) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板参数包裹为微信要求的 {"xxx": {"value": "..."}} 结构。
|
||||||
|
*/
|
||||||
|
private Map<String, Object> wrapTemplateData(Map<String, String> data) {
|
||||||
|
Map<String, Object> wrapped = new LinkedHashMap<>();
|
||||||
|
data.forEach((key, value) -> wrapped.put(key, Map.of(TMPL_FIELD_VALUE_KEY, value)));
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/com/haisen/laop/common/BusinessException.java
Normal file
43
src/main/java/com/haisen/laop/common/BusinessException.java
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.haisen.laop.common;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常,用于在 Service 层主动抛出以终止流程并返回错误信息。
|
||||||
|
*
|
||||||
|
* <p>由 {@link GlobalExceptionHandler} 统一捕获并转换为 {@link Result} 响应,
|
||||||
|
* 前端根据 {@code message} 展示用户提示。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务错误码,默认 500;可自定义以支持前端按码分支处理(如 1001=参数校验失败)。
|
||||||
|
*/
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造业务异常(默认错误码 500)。
|
||||||
|
*
|
||||||
|
* @param message 用户可见的错误提示
|
||||||
|
*/
|
||||||
|
public BusinessException(String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造指定错误码的业务异常。
|
||||||
|
*
|
||||||
|
* @param code 业务错误码
|
||||||
|
* @param message 用户可见的错误提示
|
||||||
|
*/
|
||||||
|
public BusinessException(Integer code, String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.haisen.laop.common;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局异常处理器,统一拦截 Controller 层抛出的异常并转换为 {@link Result} 响应。
|
||||||
|
*
|
||||||
|
* <p>职责:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>捕获 {@link BusinessException} — 业务主动抛出的异常,直接返回其错误码和消息</li>
|
||||||
|
* <li>捕获参数校验异常 — 返回首个校验失败的错误消息</li>
|
||||||
|
* <li>兜底捕获 {@link Exception} — 返回 500 通用错误,同时记录完整堆栈便于排查</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常,直接透传 code 和 message。
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public Result<Void> handleBusinessException(BusinessException ex) {
|
||||||
|
log.warn("[业务异常] code={}, msg={}", ex.getCode(), ex.getMessage());
|
||||||
|
return Result.fail(ex.getCode(), ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数校验异常(@Valid @Validated),返回首个校验失败的消息。
|
||||||
|
*/
|
||||||
|
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
|
||||||
|
public Result<Void> handleValidationException(Exception ex) {
|
||||||
|
String message = extractFirstErrorMessage(ex);
|
||||||
|
log.warn("[参数校验失败] {}", message);
|
||||||
|
return Result.fail(1001, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兜底异常处理器,捕获所有未被上层处理的异常。
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public Result<Void> handleException(Exception ex) {
|
||||||
|
log.error("[系统异常] ", ex);
|
||||||
|
return Result.fail("系统繁忙,请稍后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从校验异常中提取首个错误消息。
|
||||||
|
*/
|
||||||
|
private String extractFirstErrorMessage(Exception ex) {
|
||||||
|
if (ex instanceof MethodArgumentNotValidException methodEx) {
|
||||||
|
return methodEx.getBindingResult().getFieldErrors().stream()
|
||||||
|
.findFirst()
|
||||||
|
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||||
|
.orElse("参数校验失败");
|
||||||
|
}
|
||||||
|
if (ex instanceof BindException bindEx) {
|
||||||
|
return bindEx.getBindingResult().getFieldErrors().stream()
|
||||||
|
.findFirst()
|
||||||
|
.map(error -> error.getField() + ": " + error.getDefaultMessage())
|
||||||
|
.orElse("参数校验失败");
|
||||||
|
}
|
||||||
|
return ex.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/java/com/haisen/laop/common/RequestContext.java
Normal file
50
src/main/java/com/haisen/laop/common/RequestContext.java
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.haisen.laop.common;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求上下文,基于 {@link ThreadLocal} 持有当前登录用户 ID。
|
||||||
|
*
|
||||||
|
* <p>由鉴权拦截器在请求进入时写入,请求结束时清理;业务代码通过 {@link #getUserId()} 获取,
|
||||||
|
* 避免在 Controller 层层层传递 userId。</p>
|
||||||
|
*
|
||||||
|
* <p><b>设计说明:</b>当前阶段 token 为前端直接传入的 userId(简化实现),后续升级为 JWT 时
|
||||||
|
* 拦截器内部替换为 JWT 解析逻辑即可,业务层调用点无需改动。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class RequestContext {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThreadLocal 存储当前请求的用户 ID。
|
||||||
|
*/
|
||||||
|
private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
private RequestContext() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前请求的用户 ID。
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
*/
|
||||||
|
public static void setUserId(Long userId) {
|
||||||
|
USER_ID_HOLDER.set(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前请求的用户 ID。
|
||||||
|
*
|
||||||
|
* @return 当前用户 ID,未设置时返回 null
|
||||||
|
*/
|
||||||
|
public static Long getUserId() {
|
||||||
|
return USER_ID_HOLDER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除当前请求的上下文,防止 Thread 复用时污染后续请求。
|
||||||
|
* <p>必须在请求结束时调用(如拦截器 afterCompletion)。</p>
|
||||||
|
*/
|
||||||
|
public static void clear() {
|
||||||
|
USER_ID_HOLDER.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/main/java/com/haisen/laop/common/Result.java
Normal file
85
src/main/java/com/haisen/laop/common/Result.java
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.haisen.laop.common;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一响应封装,前后端契约对齐。
|
||||||
|
*
|
||||||
|
* <p>约定:{@code code = 200} 表示成功;{@code 401} 表示未授权(token 过期/无效);
|
||||||
|
* 其他非 200 表示业务异常,前端根据 {@code message} 展示提示。</p>
|
||||||
|
*
|
||||||
|
* @param <T> 业务数据类型
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Result<T> implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应码,200 成功,401 未授权,其他非 200 为业务错误。
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应消息,失败时展示给用户。
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务数据,成功时携带;失败时通常为 null。
|
||||||
|
*/
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(无数据)。
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success() {
|
||||||
|
return build(200, "success", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(携带数据)。
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success(T data) {
|
||||||
|
return build(200, "success", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功响应(携带数据和自定义消息)。
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> success(T data, String message) {
|
||||||
|
return build(200, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务失败响应。
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> fail(String message) {
|
||||||
|
return build(500, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定错误码的失败响应。
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> fail(Integer code, String message) {
|
||||||
|
return build(code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未授权响应(token 过期或无效)。
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> unauthorized(String message) {
|
||||||
|
return build(401, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Result<T> build(Integer code, String message, T data) {
|
||||||
|
Result<T> result = new Result<>();
|
||||||
|
result.setCode(code);
|
||||||
|
result.setMessage(message);
|
||||||
|
result.setData(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告领域常量类,承载 {@code announcement} 表相关的分页上限、长度限制等参数。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>通用参数统一放此类;公告显示状态由
|
||||||
|
* {@code com.haisen.laop.common.enums.AnnouncementStatusEnum} 维护,
|
||||||
|
* 禁止在 Service/Controller 中出现裸数字(如 {@code .status(1)})。
|
||||||
|
* 类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class AnnouncementConstants {
|
||||||
|
|
||||||
|
/** 列表默认页大小。 */
|
||||||
|
public static final int DEFAULT_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
/** 列表最大页大小,防止一次拉取全量数据。 */
|
||||||
|
public static final int MAX_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
/** 公告标题最大长度(与 DDL varchar(100) 对齐)。 */
|
||||||
|
public static final int MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
|
/** 公告正文最大字符数,覆盖长文本的合理上限。 */
|
||||||
|
public static final int MAX_CONTENT_LENGTH = 5000;
|
||||||
|
|
||||||
|
/** 小程序端公告列表默认拉取条数(首页公告条取最新一条)。 */
|
||||||
|
public static final int MINI_LIST_DEFAULT_LIMIT = 5;
|
||||||
|
|
||||||
|
/** 小程序端公告列表单次最大拉取条数,防止拉取全量。 */
|
||||||
|
public static final int MINI_LIST_MAX_LIMIT = 20;
|
||||||
|
|
||||||
|
private AnnouncementConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图领域常量类,承载 {@code app_banner} 表相关的跳转参数、排序、长度限制等。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>通用参数统一放此类;轮播图显示状态由
|
||||||
|
* {@code com.haisen.laop.common.enums.BannerStatusEnum} 维护,
|
||||||
|
* 禁止在 Service/Controller 中出现裸数字(如 {@code .status(1)})。
|
||||||
|
* 类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class AppBannerConstants {
|
||||||
|
|
||||||
|
/** 列表默认页大小。 */
|
||||||
|
public static final int DEFAULT_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
/** 列表最大页大小,防止一次拉取全量数据。 */
|
||||||
|
public static final int MAX_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
/** 标题最大长度(与 DDL varchar(100) 对齐)。 */
|
||||||
|
public static final int MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
|
/** 副标题最大长度。 */
|
||||||
|
public static final int MAX_SUBTITLE_LENGTH = 200;
|
||||||
|
|
||||||
|
/** 自定义跳转 URL 最大长度。 */
|
||||||
|
public static final int MAX_LINK_URL_LENGTH = 500;
|
||||||
|
|
||||||
|
/** 排序值起始值,新增轮播图默认排在末尾。 */
|
||||||
|
public static final int DEFAULT_SORT_ORDER = 0;
|
||||||
|
|
||||||
|
private AppBannerConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证领域常量类,承载飞手/商家认证领域的业务错误码与数量上限等参数。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>认证状态统一由 {@code com.haisen.laop.common.enums.CertStatusEnum}
|
||||||
|
* 维护,执照类型统一由 {@code com.haisen.laop.common.enums.LicenseTypeEnum} 维护,
|
||||||
|
* 本类不再定义任何类型/状态编码,禁止在 Service/Controller 中出现裸数字判断
|
||||||
|
* (如 {@code certStatus == 2})。类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class CertConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务错误码:未认证飞手申请接单(前端据此弹"完成飞手认证"引导弹窗,而非普通错误提示)。
|
||||||
|
* 与通用 500 区分:40301 属产品引导场景,前端有专属交互。
|
||||||
|
*/
|
||||||
|
public static final int CODE_NOT_PILOT_CERTIFIED = 40301;
|
||||||
|
|
||||||
|
/** 单个飞手最多可提交的执照数量上限。 */
|
||||||
|
public static final int MAX_LICENSE_COUNT = 3;
|
||||||
|
|
||||||
|
private CertConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程领域常量类,承载 {@code course} 表相关的排课规则、分页上限、长度限制等参数。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>通用参数统一放此类;课程上下架状态由 {@code CourseStatusEnum}、
|
||||||
|
* 预约状态由 {@code AppointmentStatusEnum}、排课方式由 {@code ScheduleTypeEnum}、
|
||||||
|
* 精选标记由 {@code YesNoEnum} 维护,禁止在 Service/Controller 中出现裸数字
|
||||||
|
* (如 {@code .setStatus(1)}、{@code scheduleType == 2})。类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class CourseConstants {
|
||||||
|
|
||||||
|
/** 每周星期编码下限(1=周一,与 {@link java.time.DayOfWeek#MONDAY} 编码一致)。 */
|
||||||
|
public static final int WEEK_DAY_MIN = 1;
|
||||||
|
|
||||||
|
/** 每周星期编码上限(7=周日,与 {@link java.time.DayOfWeek#SUNDAY} 编码一致)。 */
|
||||||
|
public static final int WEEK_DAY_MAX = 7;
|
||||||
|
|
||||||
|
/** 日期排课最大日期数量,防止恶意/误操作提交超长排课串撑爆字段。 */
|
||||||
|
public static final int MAX_SCHEDULE_DATE_COUNT = 60;
|
||||||
|
|
||||||
|
/** 排课日期格式(与 schedule_dates 分号串内的单个日期格式一致)。 */
|
||||||
|
public static final DateTimeFormatter SCHEDULE_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||||
|
|
||||||
|
// ==================== 分页参数 ====================
|
||||||
|
|
||||||
|
/** 列表默认页大小。 */
|
||||||
|
public static final int DEFAULT_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
/** 列表最大页大小,防止一次拉取全量数据。 */
|
||||||
|
public static final int MAX_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
// ==================== 其他 ====================
|
||||||
|
|
||||||
|
/** 多选字段(星期串/日期串)统一分隔符:英文分号。 */
|
||||||
|
public static final String SEPARATOR = ";";
|
||||||
|
|
||||||
|
/** 课程简介(富文本 HTML)最大字符数,覆盖图文混排的合理上限。 */
|
||||||
|
public static final int MAX_DESCRIPTION_LENGTH = 20000;
|
||||||
|
|
||||||
|
/** 小程序首页精选课程列表默认上限(避免一次拉取过多数据)。 */
|
||||||
|
public static final int FEATURED_LIST_LIMIT = 6;
|
||||||
|
|
||||||
|
private CourseConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件领域常量类,承载 {@code sys_file} 相关的大小、后缀等校验参数。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>通用参数统一放此类;文件状态由
|
||||||
|
* {@code com.haisen.laop.common.enums.FileStatusEnum} 维护,
|
||||||
|
* 禁止在 Service 中出现裸数字(如 {@code .status(1)})。
|
||||||
|
* 类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class FileConstants {
|
||||||
|
|
||||||
|
/** 单文件大小上限:5MB。 */
|
||||||
|
public static final long MAX_FILE_SIZE = 5L * 1024 * 1024;
|
||||||
|
|
||||||
|
/** 允许上传的图片后缀集合(小写、不含点)。 */
|
||||||
|
public static final Set<String> ALLOWED_IMAGE_SUFFIXES = Set.of(
|
||||||
|
"jpg", "jpeg", "png", "gif", "webp", "bmp"
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 允许上传的文件后缀集合(预留非图片类型时可扩展)。 */
|
||||||
|
public static final Set<String> ALLOWED_FILE_SUFFIXES = Set.of(
|
||||||
|
"jpg", "jpeg", "png", "gif", "webp", "bmp", "pdf", "doc", "docx"
|
||||||
|
);
|
||||||
|
|
||||||
|
private FileConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页展示领域常量类,承载"实力商家 / 本地飞手"版块的数量限制等参数。
|
||||||
|
*
|
||||||
|
* <p>类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class HomeConstants {
|
||||||
|
|
||||||
|
/** 首页"实力商家"展示数量上限。 */
|
||||||
|
public static final int HOME_MERCHANT_LIMIT = 10;
|
||||||
|
|
||||||
|
/** 首页"本地飞手"展示数量上限。 */
|
||||||
|
public static final int HOME_PILOT_LIMIT = 10;
|
||||||
|
|
||||||
|
/** 详情页任务列表展示数量上限(商家发布的任务 / 飞手参与的任务)。 */
|
||||||
|
public static final int DETAIL_TASK_LIMIT = 20;
|
||||||
|
|
||||||
|
private HomeConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分领域常量类,承载积分业务的错误码、固定积分规则、功能配置键与数值边界。
|
||||||
|
*
|
||||||
|
* <p><b>积分值来源约定:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>运营可调参数(发布任务奖励、优质任务扣减)放 {@code app_config},配置键与默认值收口在本类;</li>
|
||||||
|
* <li>协议/业务固定的积分规则(邀请注册、完成任务奖励)以常量形式保留在本类;</li>
|
||||||
|
* <li>事件类型见 {@link com.haisen.laop.common.enums.PointsEventEnum},
|
||||||
|
* 变动方向见 {@code com.haisen.laop.common.enums.PointsChangeTypeEnum},
|
||||||
|
* 禁止在业务代码中出现裸积分数字。</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class PointsConstants {
|
||||||
|
|
||||||
|
/** 业务错误码:积分不足,前端识别此 code 后弹引导弹窗(如发布优质任务引导推广拉新)。 */
|
||||||
|
public static final int ERROR_CODE_POINTS_INSUFFICIENT = 10001;
|
||||||
|
|
||||||
|
/** 积分不足时的默认提示文案(与前端"推广拉新得积分"引导弹窗呼应)。 */
|
||||||
|
public static final String MSG_POINTS_INSUFFICIENT =
|
||||||
|
"积分不足,推广小程序引导好友成功注册可得积分,快去推广吧!";
|
||||||
|
|
||||||
|
// ==================== 固定积分规则(不随运营配置变化) ====================
|
||||||
|
|
||||||
|
/** 邀请好友注册成功奖励积分。 */
|
||||||
|
public static final int INVITE_REGISTER_POINTS = 4;
|
||||||
|
|
||||||
|
/** 飞手完成任务奖励积分。 */
|
||||||
|
public static final int TASK_COMPLETE_POINTS = 1;
|
||||||
|
|
||||||
|
// ==================== 功能配置键(app_config.config_key) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能配置键:发布任务获取积分数量。
|
||||||
|
* <p>用户每发布一个普通任务奖励的积分,非负整数;{@code 0} 表示不奖励。默认 {@value #DEFAULT_PUBLISH_AWARD_POINTS}。</p>
|
||||||
|
*/
|
||||||
|
public static final String CONFIG_KEY_POINTS_PUBLISH_AWARD = "points_publish_award";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能配置键:发布任务扣减积分数量。
|
||||||
|
* <p>发布优质任务消耗的积分,非负整数;{@code 0} 表示不扣减。默认 {@value #DEFAULT_PREMIUM_DEDUCT_POINTS}。</p>
|
||||||
|
*/
|
||||||
|
public static final String CONFIG_KEY_POINTS_PREMIUM_DEDUCT = "points_premium_deduct";
|
||||||
|
|
||||||
|
/** 发布任务获取积分数量的兜底默认值(配置缺失或非法时使用)。 */
|
||||||
|
public static final int DEFAULT_PUBLISH_AWARD_POINTS = 1;
|
||||||
|
|
||||||
|
/** 发布任务扣减积分数量的兜底默认值(配置缺失或非法时使用)。 */
|
||||||
|
public static final int DEFAULT_PREMIUM_DEDUCT_POINTS = 4;
|
||||||
|
|
||||||
|
// ==================== 数值边界 ====================
|
||||||
|
|
||||||
|
/** 积分规则配置项允许的最小值(0 表示不奖励/不扣减)。 */
|
||||||
|
public static final int MIN_CONFIG_POINTS = 0;
|
||||||
|
|
||||||
|
/** 积分规则配置项允许的最大值。 */
|
||||||
|
public static final int MAX_CONFIG_POINTS = 99999;
|
||||||
|
|
||||||
|
/** Web 后台单次手动增减积分允许的最小值(手动调整必须为正整数,方向另传)。 */
|
||||||
|
public static final int MIN_MANUAL_ADJUST_POINTS = 1;
|
||||||
|
|
||||||
|
/** Web 后台单次手动增减积分允许的最大值。 */
|
||||||
|
public static final int MAX_MANUAL_ADJUST_POINTS = 99999;
|
||||||
|
|
||||||
|
/** 手动调整原因最大长度(points_detail.remark 列长 200,人工原因从严限制 100)。 */
|
||||||
|
public static final int MAX_MANUAL_REMARK_LENGTH = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取值为"非负整数"的配置键集合,AppConfigService 更新时据此做数值校验。
|
||||||
|
*/
|
||||||
|
public static final Set<String> NUMERIC_CONFIG_KEYS = Set.of(
|
||||||
|
CONFIG_KEY_POINTS_PUBLISH_AWARD,
|
||||||
|
CONFIG_KEY_POINTS_PREMIUM_DEDUCT);
|
||||||
|
|
||||||
|
private PointsConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务领域常量类,承载 {@code task} 相关的业务默认值、上限下限等参数。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>通用参数统一放此类;任务状态由 {@code com.haisen.laop.common.enums.TaskStatusEnum}
|
||||||
|
* 维护、申请记录状态由 {@code TaskRecordStatusEnum} 维护、0/1 标记位由 {@code YesNoEnum} 维护,
|
||||||
|
* 禁止在 Service/Controller 中出现裸数字(如 {@code .premiumCost(0)})。
|
||||||
|
* 类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class TaskConstants {
|
||||||
|
|
||||||
|
/** 场景图最多上传数量(同时也限制 {@code scene_image_ids} 分号串长度)。 */
|
||||||
|
public static final int MAX_SCENE_IMAGE_COUNT = 5;
|
||||||
|
|
||||||
|
/** 需求人数上下限。 */
|
||||||
|
public static final int REQUIRED_COUNT_MIN = 1;
|
||||||
|
public static final int REQUIRED_COUNT_MAX = 99;
|
||||||
|
|
||||||
|
/** 任务编号前缀。 */
|
||||||
|
public static final String TASK_NO_PREFIX = "LAOP";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务编号尾部(毫秒后4位)的补零长度:确保始终为 4 位(如 0089、5678)。
|
||||||
|
*
|
||||||
|
* <p>编号整体规则:{@code LAOP + yyyyMMdd + 毫秒后4位},共 13 位,如 {@code LAOP202608285678}。</p>
|
||||||
|
*/
|
||||||
|
public static final int TASK_NO_TAIL_LENGTH = 4;
|
||||||
|
|
||||||
|
// ==================== 申请记录(task_record)状态 ====================
|
||||||
|
// 申请记录状态编码统一由枚举 com.haisen.laop.common.enums.TaskRecordStatusEnum
|
||||||
|
// 维护(APPLIED=1 / APPROVED=2 / REJECTED=3 / WITHDRAWN=4),
|
||||||
|
// 业务代码通过 TaskRecordStatusEnum.XXX.getCode() 取编码写入数据库(status 仍为 SMALLINT),
|
||||||
|
// 禁止在此处重新定义 int 常量,避免编码与含义两处维护产生漂移。
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同意某位飞手后,其余"已申请"记录自动关闭的拒绝原因文案。
|
||||||
|
*/
|
||||||
|
public static final String AUTO_REJECT_REASON = "商家已选择其他飞手";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手撤销申请时写入 cancel_reason 的统一文案(申诉与风控审计依据)。
|
||||||
|
*/
|
||||||
|
public static final String WITHDRAW_REASON_PILOT = "飞手主动撤销申请";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家撤回任务时写入申请记录 reject_reason 的统一文案。
|
||||||
|
* 已接单的飞手会收到任务撤回通知,申请记录状态同步关闭。
|
||||||
|
*/
|
||||||
|
public static final String RECALL_REASON_MERCHANT = "商家撤回任务";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家确认开始任务时,自动关闭待确认申请的 reject_reason 统一文案。
|
||||||
|
*/
|
||||||
|
public static final String AUTO_REJECT_REASON_TASK_STARTED = "任务已开始";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个任务的申请人数上限(防刷防御值,达到后不再接受新申请)。
|
||||||
|
*/
|
||||||
|
public static final int MAX_APPLICANT_COUNT = 50;
|
||||||
|
|
||||||
|
private TaskConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户领域常量类,承载 {@code "user"} 表相关的分页上限等参数。
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>通用参数统一放此类;角色类型由 {@code RoleTypeEnum}、
|
||||||
|
* 性别由 {@code GenderEnum}、账号状态由 {@code UserStatusEnum} 维护,
|
||||||
|
* 禁止在 Service/Controller 中出现裸数字(如 {@code roleType == 2})。
|
||||||
|
* 类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class UserConstants {
|
||||||
|
|
||||||
|
/** 列表默认页大小。 */
|
||||||
|
public static final int DEFAULT_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
/** 列表最大页大小,防止一次拉取全量数据。 */
|
||||||
|
public static final int MAX_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
private UserConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
package com.haisen.laop.common.constants;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信服务号模板消息领域常量类,承载模板字段 key、回调事件名、功能配置键等魔法字符串。
|
||||||
|
*
|
||||||
|
* <p><b>模板消息链路:</b>业务事件 → DB 持久化队列 → 消费者调微信
|
||||||
|
* {@code /cgi-bin/message/template/send} 提交 → 微信异步回调
|
||||||
|
* {@code TEMPLATESENDJOBFINISH} 送达结果 → 回写发送日志。</p>
|
||||||
|
*
|
||||||
|
* <p><b>设计规则:</b>模板字段 key(如 {@code character_string14})、事件名等魔法字符串
|
||||||
|
* 统一收口此类,禁止在业务代码中裸写;业务类型、发送状态、任务状态等数字编码统一由
|
||||||
|
* {@code WxNotifyBizTypeEnum}、{@code WxSendStatusEnum}、{@code WxNotifyTaskStatusEnum}
|
||||||
|
* 维护。类私有无参构造器,防止被实例化。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public final class WxMessageConstants {
|
||||||
|
|
||||||
|
// ==================== 任务申请通知模板字段 key ====================
|
||||||
|
|
||||||
|
/** 模板字段:任务编号(character_string14)。 */
|
||||||
|
public static final String TMPL_TASK_NO = "character_string14";
|
||||||
|
|
||||||
|
/** 模板字段:任务名称(thing5,微信 thing 类型限 20 字符)。 */
|
||||||
|
public static final String TMPL_TASK_NAME = "thing5";
|
||||||
|
|
||||||
|
/** 模板字段:服务类型(thing12,如 植保、吊运)。 */
|
||||||
|
public static final String TMPL_SERVICE_TYPE = "thing12";
|
||||||
|
|
||||||
|
/** 模板字段:处理人(thing17,飞手认证姓名 + 电话)。 */
|
||||||
|
public static final String TMPL_HANDLER = "thing17";
|
||||||
|
|
||||||
|
/** 模板字段:接收时间(time18,yyyy-MM-dd HH:mm)。 */
|
||||||
|
public static final String TMPL_APPLY_TIME = "time18";
|
||||||
|
|
||||||
|
// ==================== 服务号用户字段 ====================
|
||||||
|
|
||||||
|
/** 服务号用户 subscribe 字段值:1 = 已关注(微信 API 返回值,非平台业务状态)。 */
|
||||||
|
public static final int MP_SUBSCRIBED = 1;
|
||||||
|
|
||||||
|
// ==================== 服务号回调事件 ====================
|
||||||
|
|
||||||
|
/** 事件类型字段名(XML)。 */
|
||||||
|
public static final String XML_FIELD_EVENT = "Event";
|
||||||
|
|
||||||
|
/** 模板消息送达结果事件:TEMPLATESENDJOBFINISH。 */
|
||||||
|
public static final String EVENT_TEMPLATE_SEND_JOB_FINISH = "TEMPLATESENDJOBFINISH";
|
||||||
|
|
||||||
|
/** 关注事件:subscribe(用于自动绑定服务号 openid)。 */
|
||||||
|
public static final String EVENT_SUBSCRIBE = "subscribe";
|
||||||
|
|
||||||
|
/** 取关事件:unsubscribe(用于自动解绑服务号 openid)。 */
|
||||||
|
public static final String EVENT_UNSUBSCRIBE = "unsubscribe";
|
||||||
|
|
||||||
|
/** 送达结果字段名(XML)。 */
|
||||||
|
public static final String XML_FIELD_STATUS = "Status";
|
||||||
|
|
||||||
|
/** 送达成功 Status 值。 */
|
||||||
|
public static final String STATUS_SUCCESS = "success";
|
||||||
|
|
||||||
|
/** 送达失败 Status 前缀(如 failed:user block、failed: system failed)。 */
|
||||||
|
public static final String STATUS_FAILED_PREFIX = "failed";
|
||||||
|
|
||||||
|
/** 用户拒收事件(软性失败,微信可能延迟重试后推送最终结果)。 */
|
||||||
|
public static final String STATUS_USER_BLOCK = "failed:user block";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML 事件消息中允许处理的合法事件名集合(防御性校验,避免处理无关事件)。
|
||||||
|
*/
|
||||||
|
public static final Set<String> HANDLED_EVENTS = Set.of(
|
||||||
|
EVENT_TEMPLATE_SEND_JOB_FINISH, EVENT_SUBSCRIBE, EVENT_UNSUBSCRIBE);
|
||||||
|
|
||||||
|
// ==================== 功能配置键(app_config.config_key) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 功能配置键:微信公众号模板消息推送总开关({@code app_config.mp_push_enabled})。
|
||||||
|
* <p>取值规则:{@code "1"}=开启(消费者正常轮询 wx_notify_task),
|
||||||
|
* {@code "0"}=关闭(消费者跳过 DB 操作,仅按轮询间隔空转监听开关状态)。
|
||||||
|
* 由 Web 端功能配置入口维护,运行时可切换。</p>
|
||||||
|
*/
|
||||||
|
public static final String CONFIG_KEY_MP_PUSH_ENABLED = "mp_push_enabled";
|
||||||
|
|
||||||
|
private WxMessageConstants() {
|
||||||
|
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告状态枚举,对应 {@code announcement.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum AnnouncementStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏,小程序端不展示。
|
||||||
|
*/
|
||||||
|
HIDDEN(0, "隐藏"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示,小程序端首页公告条可见。
|
||||||
|
*/
|
||||||
|
VISIBLE(1, "显示");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
AnnouncementStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static AnnouncementStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (AnnouncementStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程预约状态枚举,对应 {@code course_appointment.status} 字段。
|
||||||
|
*
|
||||||
|
* <p>二态模型:用户提交预约即生效(无需商家确认),取消为终态。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum AppointmentStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已预约,用户提交即生效,无需商家确认。
|
||||||
|
*/
|
||||||
|
ACTIVE(1, "已预约"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已取消,终态,用户可自助取消。
|
||||||
|
*/
|
||||||
|
CANCELLED(2, "已取消");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 course_appointment.status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
AppointmentStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static AppointmentStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (AppointmentStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图状态枚举,对应 {@code app_banner.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum BannerStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏,小程序端不展示。
|
||||||
|
*/
|
||||||
|
HIDDEN(0, "隐藏"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示,小程序端首页轮播可见。
|
||||||
|
*/
|
||||||
|
VISIBLE(1, "显示");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
BannerStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static BannerStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (BannerStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证状态枚举,对应 {@code pilot.cert_status} / {@code merchant.cert_status} 字段。
|
||||||
|
*
|
||||||
|
* <p>认证状态机:未认证(0)→ 认证中(1)→ 已认证(2)/ 认证失败(3);
|
||||||
|
* 已认证可被运营吊销为(4),吊销后如需恢复由管理员改回已认证。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum CertStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未认证,从未提交过认证资料。
|
||||||
|
*/
|
||||||
|
NONE(0, "未认证"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证中,资料已提交等待审核。
|
||||||
|
*/
|
||||||
|
PENDING(1, "认证中"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已认证,审核通过,业务权限放行。
|
||||||
|
*/
|
||||||
|
CERTIFIED(2, "已认证"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证失败,审核驳回,可修改后重新提交。
|
||||||
|
*/
|
||||||
|
REJECTED(3, "认证失败"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已吊销,运营后台强制取消资格,认证记录保留不软删。
|
||||||
|
*/
|
||||||
|
REVOKED(4, "已吊销");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 cert_status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
CertStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static CertStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CertStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 研学课程分类枚举,对应 {@code course.category} 字段。
|
||||||
|
*
|
||||||
|
* <p>编码与 {@code Course} 实体注释一致,用于小程序课程卡片/详情展示分类标签。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum CourseCategoryEnum {
|
||||||
|
|
||||||
|
COGNITION(1, "认知启蒙"),
|
||||||
|
SKILL(2, "技能进阶"),
|
||||||
|
INNOVATION(3, "创新应用"),
|
||||||
|
COMPETITION(4, "竞赛辅导");
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
CourseCategoryEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,非法编码返回 null。
|
||||||
|
*/
|
||||||
|
public static CourseCategoryEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CourseCategoryEnum category : values()) {
|
||||||
|
if (category.code.equals(code)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取分类中文名,非法/空编码返回 null。
|
||||||
|
*/
|
||||||
|
public static String nameOf(Integer code) {
|
||||||
|
CourseCategoryEnum category = of(code);
|
||||||
|
return category == null ? null : category.desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程状态枚举,对应 {@code course.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum CourseStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下架,管理端可切换,小程序端不可见。
|
||||||
|
*/
|
||||||
|
OFF_SHELF(0, "下架"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上架,小程序端可见可预约。
|
||||||
|
*/
|
||||||
|
ON_SHELF(1, "上架"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已满员,预约名额已满,仅展示不允许预约。
|
||||||
|
*/
|
||||||
|
FULL(2, "已满员");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 course.status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
CourseStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static CourseStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CourseStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题库状态枚举,对应 {@code exam_bank.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum ExamBankStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用,不对小程序展示。
|
||||||
|
*/
|
||||||
|
DISABLED(0, "禁用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用。
|
||||||
|
*/
|
||||||
|
ENABLED(1, "启用");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
ExamBankStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static ExamBankStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (ExamBankStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目/题库难度枚举,对应 {@code exam_question.difficulty} 与 {@code exam_bank.difficulty} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum ExamDifficultyEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单。
|
||||||
|
*/
|
||||||
|
SIMPLE(1, "简单"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中等。
|
||||||
|
*/
|
||||||
|
MEDIUM(2, "中等"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 困难。
|
||||||
|
*/
|
||||||
|
HARD(3, "困难");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 difficulty 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
ExamDifficultyEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static ExamDifficultyEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (ExamDifficultyEnum difficulty : values()) {
|
||||||
|
if (difficulty.code.equals(code)) {
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件状态枚举,对应 {@code sys_file.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum FileStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用,违规文件不可访问。
|
||||||
|
*/
|
||||||
|
DISABLED(0, "禁用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正常。
|
||||||
|
*/
|
||||||
|
NORMAL(1, "正常");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
FileStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static FileStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (FileStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/main/java/com/haisen/laop/common/enums/FileTypeEnum.java
Normal file
114
src/main/java/com/haisen/laop/common/enums/FileTypeEnum.java
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件类型枚举,对应 {@code sys_file.file_type} 字段。
|
||||||
|
*
|
||||||
|
* <p><b>设计说明:</b>前端请求时传字符串标识(如 {@code SCENE_IMAGE}),后端由此枚举
|
||||||
|
* 统一转为数字编码存储,避免两端硬编码数字含义不一致。</p>
|
||||||
|
*
|
||||||
|
* <p><b>存储目录规划:</b>每种类型映射到磁盘上的大类子目录({@code storageDir}),
|
||||||
|
* 按"媒体形态 + 业务用途"归组,避免所有文件堆在同一个目录:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code image/} —— 通用图片(任务场景图、课程图、头像等)</li>
|
||||||
|
* <li>{@code video/} —— 视频(预留,当前上传白名单仅放行图片)</li>
|
||||||
|
* <li>{@code license/} —— 执照照片(营业执照、飞手执照)</li>
|
||||||
|
* <li>{@code store/} —— 门店照片(商家门头照)</li>
|
||||||
|
* </ul></p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum FileTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 营业执照图片,商家认证上传。
|
||||||
|
*/
|
||||||
|
BUSINESS_LICENSE(1, "business_license", "license"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手执照图片,飞手认证上传。
|
||||||
|
*/
|
||||||
|
PILOT_LICENSE(2, "pilot_license", "license"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务作业场景图,发布任务时上传。
|
||||||
|
*/
|
||||||
|
SCENE_IMAGE(3, "scene_image", "image"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程封面/详情图片。
|
||||||
|
*/
|
||||||
|
COURSE_IMAGE(4, "course_image", "image"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户头像。
|
||||||
|
*/
|
||||||
|
AVATAR(5, "avatar", "image"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家门头照片,商家认证上传。
|
||||||
|
*/
|
||||||
|
DOOR_IMAGE(6, "door_image", "store"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频(预留类型:目录规划已就绪,上传白名单按需放行视频格式后启用)。
|
||||||
|
*/
|
||||||
|
VIDEO(7, "video", "video"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用文件(未分类兜底)。
|
||||||
|
*/
|
||||||
|
OTHER(9, "other", "other"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页轮播图,运营后台上传。
|
||||||
|
*/
|
||||||
|
BANNER_IMAGE(8, "banner_image", "image"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 培训门店封面图,培训考试模块上传。
|
||||||
|
*/
|
||||||
|
TRAINING_COVER(10, "training_cover", "image");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 sys_file.file_type */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 字符串标识,前后端约定的类型 key */
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
/** 磁盘存储子目录名(staticDir/storageDir/uuid.suffix) */
|
||||||
|
private final String storageDir;
|
||||||
|
|
||||||
|
FileTypeEnum(Integer code, String key, String storageDir) {
|
||||||
|
this.code = code;
|
||||||
|
this.key = key;
|
||||||
|
this.storageDir = storageDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字符串标识查找枚举实例。
|
||||||
|
*
|
||||||
|
* @param key 前端传入的字符串标识,如 {@code SCENE_IMAGE}
|
||||||
|
* @return 枚举实例,找不到返回 null
|
||||||
|
*/
|
||||||
|
public static FileTypeEnum of(String key) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (FileTypeEnum type : values()) {
|
||||||
|
if (type.name().equals(key) || type.key.equalsIgnoreCase(key)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验字符串标识是否合法。
|
||||||
|
*/
|
||||||
|
public static boolean isValid(String key) {
|
||||||
|
return of(key) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/main/java/com/haisen/laop/common/enums/GenderEnum.java
Normal file
53
src/main/java/com/haisen/laop/common/enums/GenderEnum.java
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户性别枚举,对应 {@code "user".gender} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum GenderEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未知,微信未授权性别时的默认值。
|
||||||
|
*/
|
||||||
|
UNKNOWN(0, "未知"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 男。
|
||||||
|
*/
|
||||||
|
MALE(1, "男"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 女。
|
||||||
|
*/
|
||||||
|
FEMALE(2, "女");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 gender 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
GenderEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static GenderEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (GenderEnum gender : values()) {
|
||||||
|
if (gender.code.equals(code)) {
|
||||||
|
return gender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保险类型枚举,对应 {@code task.insurance_require} 分号拼接串中的数字编码。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum InsuranceTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三者责任险,覆盖无人机对第三方造成的人身/财产损害。
|
||||||
|
*/
|
||||||
|
THIRD_PARTY(1, "三者险"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手责任险,覆盖飞手自身操作失误。
|
||||||
|
*/
|
||||||
|
PILOT(2, "飞手险"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机身险,覆盖无人机自身损坏/丢失。
|
||||||
|
*/
|
||||||
|
AIRFRAME(3, "机身险");
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
InsuranceTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例。
|
||||||
|
*/
|
||||||
|
public static InsuranceTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (InsuranceTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验编码是否属于合法的保险类型。
|
||||||
|
*/
|
||||||
|
public static boolean isValid(Integer code) {
|
||||||
|
return of(code) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞行执照类型枚举,对应 {@code pilot_license.license_type} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum LicenseTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AOPA 合格证(中国航空器拥有者及驾驶员协会)。
|
||||||
|
*/
|
||||||
|
AOPA(1, "AOPA"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTC 合格证(无人驾驶航空器系统操作手合格证)。
|
||||||
|
*/
|
||||||
|
UTC(2, "UTC"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASFC 执照(航空运动协会遥控航空模型飞行员执照)。
|
||||||
|
*/
|
||||||
|
ASFC(3, "ASFC"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAAC 执照(民航局颁发的民用无人机操控员执照)。
|
||||||
|
*/
|
||||||
|
CAAC(4, "CAAC");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 license_type 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
LicenseTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static LicenseTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (LicenseTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验编码是否属于合法的执照类型。
|
||||||
|
*/
|
||||||
|
public static boolean isValid(Integer code) {
|
||||||
|
return of(code) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分变动方向枚举,对应 {@code points_detail.change_type} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum PointsChangeTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收入,积分增加。
|
||||||
|
*/
|
||||||
|
GAIN(1, "收入"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支出,积分扣减。
|
||||||
|
*/
|
||||||
|
SPEND(2, "支出");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 change_type 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
PointsChangeTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static PointsChangeTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (PointsChangeTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分事件枚举,对应 {@code points_detail.event_type},定义事件编码、变动方向与展示名称。
|
||||||
|
*
|
||||||
|
* <p><b>设计说明:</b>本枚举只描述"是什么事件",不再承载固定积分变动值——
|
||||||
|
* 发布任务奖励/优质任务扣减已转为 {@code app_config} 运营可配置项,
|
||||||
|
* 手动增减的数值由管理员逐笔填写。各事件的积分数量由调用方
|
||||||
|
* ({@link com.haisen.laop.service.PointsService} 的调用方)显式传入:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>固定规则(邀请注册 +4、完成任务 +1)取值 {@code PointsConstants};</li>
|
||||||
|
* <li>可配置规则(发布任务奖励、优质任务扣减)运行时读取 {@code app_config}。</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum PointsEventEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家发布任务,发放配置项 points_publish_award 设定的奖励积分。
|
||||||
|
*/
|
||||||
|
PUBLISH_TASK(1, PointsChangeTypeEnum.GAIN, "发布任务"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手完成任务,发放固定奖励积分。
|
||||||
|
*/
|
||||||
|
TASK_COMPLETE(2, PointsChangeTypeEnum.GAIN, "完成任务"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邀请好友注册成功,发放固定奖励积分。
|
||||||
|
*/
|
||||||
|
INVITE_REGISTER(3, PointsChangeTypeEnum.GAIN, "邀请好友注册"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布优质任务,扣减配置项 points_premium_deduct 设定的积分。
|
||||||
|
*/
|
||||||
|
PREMIUM_TASK(4, PointsChangeTypeEnum.SPEND, "发布优质任务"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web 后台手动新增积分,数量由管理员逐笔填写。
|
||||||
|
*/
|
||||||
|
MANUAL_ADD(5, PointsChangeTypeEnum.GAIN, "手动新增"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web 后台手动扣减积分,数量由管理员逐笔填写,余额不足时拒绝扣减。
|
||||||
|
*/
|
||||||
|
MANUAL_DEDUCT(6, PointsChangeTypeEnum.SPEND, "手动扣减");
|
||||||
|
|
||||||
|
/** 事件类型编码,存入 points_detail.event_type。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 变动方向,存入 points_detail.change_type(落库取 {@link PointsChangeTypeEnum#getCode()})。 */
|
||||||
|
private final PointsChangeTypeEnum changeType;
|
||||||
|
|
||||||
|
/** 事件名称,积分明细列表直接展示。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
PointsEventEnum(Integer code, PointsChangeTypeEnum changeType, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.changeType = changeType;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按编码查找枚举实例。
|
||||||
|
*
|
||||||
|
* @param code 事件类型编码
|
||||||
|
* @return 枚举实例,找不到返回 null
|
||||||
|
*/
|
||||||
|
public static PointsEventEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (PointsEventEnum event : values()) {
|
||||||
|
if (event.code.equals(code)) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考试题型枚举,对应 {@code exam_question.question_type} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum QuestionTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单选题。
|
||||||
|
*/
|
||||||
|
SINGLE(1, "单选题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多选题。
|
||||||
|
*/
|
||||||
|
MULTI(2, "多选题"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断题。
|
||||||
|
*/
|
||||||
|
JUDGE(3, "判断题");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 question_type 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
QuestionTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static QuestionTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (QuestionTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验编码是否属于合法的题型。
|
||||||
|
*/
|
||||||
|
public static boolean isValid(Integer code) {
|
||||||
|
return of(code) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行政区划级别枚举,对应 {@code region.level} 字段(省/市/区县三级)。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum RegionLevelEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 省级行政区(省、自治区、直辖市)。
|
||||||
|
*/
|
||||||
|
PROVINCE(1, "省"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 市级行政区(地级市、自治州等)。
|
||||||
|
*/
|
||||||
|
CITY(2, "市"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区县级行政区(市辖区、县级市、县等)。
|
||||||
|
*/
|
||||||
|
DISTRICT(3, "区县");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 level 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
RegionLevelEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static RegionLevelEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (RegionLevelEnum level : values()) {
|
||||||
|
if (level.code.equals(code)) {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/main/java/com/haisen/laop/common/enums/RoleTypeEnum.java
Normal file
58
src/main/java/com/haisen/laop/common/enums/RoleTypeEnum.java
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户主角色类型枚举,对应 {@code "user".role_type} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum RoleTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 普通用户。
|
||||||
|
*/
|
||||||
|
NORMAL(1, "普通用户"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手。
|
||||||
|
*/
|
||||||
|
PILOT(2, "飞手"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家。
|
||||||
|
*/
|
||||||
|
MERCHANT(3, "商家"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构。
|
||||||
|
*/
|
||||||
|
ORG(4, "机构");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 role_type 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
RoleTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static RoleTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (RoleTypeEnum role : values()) {
|
||||||
|
if (role.code.equals(code)) {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程排课方式枚举,对应课程排课 DTO 中的 {@code scheduleType}。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum ScheduleTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每周重复,每周固定星期几上课,取值存 schedule_week_days。
|
||||||
|
*/
|
||||||
|
WEEKLY(1, "每周重复"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期排课,指定具体日期上课,取值存 schedule_dates。
|
||||||
|
*/
|
||||||
|
DATE(2, "日期排课");
|
||||||
|
|
||||||
|
/** 数字编码。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
ScheduleTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static ScheduleTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (ScheduleTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验编码是否属于合法的排课方式。
|
||||||
|
*/
|
||||||
|
public static boolean isValid(Integer code) {
|
||||||
|
return of(code) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务申请记录状态枚举,对应 {@code task_record.status} 字段。
|
||||||
|
*
|
||||||
|
* <p>申请-审批流状态机:飞手申请(1)→ 商家同意(2)/ 拒绝(3)→ 飞手可主动撤销(4)。
|
||||||
|
* 用于小程序"已接单"列表展示申请进度,与任务状态({@link TaskStatusEnum})相互独立。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum TaskRecordStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已申请,等待商家确认。
|
||||||
|
*/
|
||||||
|
APPLIED(1, "待商家确认"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家已同意,任务进入已接单。
|
||||||
|
*/
|
||||||
|
APPROVED(2, "已接单"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家已拒绝。
|
||||||
|
*/
|
||||||
|
REJECTED(3, "已拒绝"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手已撤销申请。
|
||||||
|
*/
|
||||||
|
WITHDRAWN(4, "已撤销");
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
TaskRecordStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例。
|
||||||
|
*/
|
||||||
|
public static TaskRecordStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (TaskRecordStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务状态枚举,对应 {@code task.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum TaskStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 草稿,发布前的暂存状态。
|
||||||
|
*/
|
||||||
|
DRAFT(0, "草稿"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待接单,发布成功后默认状态,等待飞手抢单。
|
||||||
|
*/
|
||||||
|
PENDING(1, "待接单"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已接单,飞手抢单成功但尚未开始执行。
|
||||||
|
*/
|
||||||
|
ACCEPTED(2, "已接单"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行中,飞手已到场并开始作业。
|
||||||
|
*/
|
||||||
|
EXECUTING(3, "执行中"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已完成,作业结束且双方确认无异议。
|
||||||
|
*/
|
||||||
|
COMPLETED(4, "已完成"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已取消,商家或系统主动终止任务。
|
||||||
|
*/
|
||||||
|
CANCELLED(5, "已取消");
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
TaskStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例。
|
||||||
|
*/
|
||||||
|
public static TaskStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (TaskStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/main/java/com/haisen/laop/common/enums/TaskTypeEnum.java
Normal file
79
src/main/java/com/haisen/laop/common/enums/TaskTypeEnum.java
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务类型枚举,对应 {@code task.type} 字段。
|
||||||
|
*
|
||||||
|
* <p>编码取值与 DDL 注释一致:1植保 2吊运 3航拍 4巡检 5清洗 6表演 7测绘。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum TaskTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 植保作业,农用无人机喷洒农药/化肥。
|
||||||
|
*/
|
||||||
|
PLANT_PROTECTION(1, "植保"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 吊运物资,短距离物资投送/吊装。
|
||||||
|
*/
|
||||||
|
LIFTING(2, "吊运"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 航拍,商业宣传片/活动/婚礼航拍。
|
||||||
|
*/
|
||||||
|
AERIAL_PHOTOGRAPHY(3, "航拍"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 巡检,电力/铁路/管道/安防等设施巡检。
|
||||||
|
*/
|
||||||
|
INSPECTION(4, "巡检"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清洗,高楼/光伏板/风力发电机清洗。
|
||||||
|
*/
|
||||||
|
CLEANING(5, "清洗"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表演,商业活动/婚礼/庆典无人机编队表演。
|
||||||
|
*/
|
||||||
|
PERFORMANCE(6, "表演"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测绘,地形/建筑/电力线路三维建模。
|
||||||
|
*/
|
||||||
|
SURVEYING(7, "测绘");
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
TaskTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static TaskTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (TaskTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验编码是否属于合法的任务类型。
|
||||||
|
*/
|
||||||
|
public static boolean isValid(Integer code) {
|
||||||
|
return of(code) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 培训门店状态枚举,对应 {@code training_store.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum TrainingStoreStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用。
|
||||||
|
*/
|
||||||
|
DISABLED(0, "禁用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用,小程序培训页展示当前唯一启用门店。
|
||||||
|
*/
|
||||||
|
ENABLED(1, "启用");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
TrainingStoreStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static TrainingStoreStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (TrainingStoreStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户账号状态枚举,对应 {@code "user".status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum UserStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用,禁止登录与业务操作。
|
||||||
|
*/
|
||||||
|
DISABLED(0, "禁用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正常。
|
||||||
|
*/
|
||||||
|
NORMAL(1, "正常");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
UserStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static UserStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (UserStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信模板消息业务类型枚举,对应 {@code wx_message_log.biz_type} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum WxNotifyBizTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务申请通知,飞手申请接单 → 通知商家。
|
||||||
|
*/
|
||||||
|
TASK_APPLY(1, "任务申请通知"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家审核结果通知,商家同意/拒绝 → 通知飞手。
|
||||||
|
*/
|
||||||
|
REVIEW_NOTIFY(2, "审核结果通知"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手撤销申请通知,飞手撤回接单申请 → 通知商家。
|
||||||
|
*/
|
||||||
|
CANCEL_NOTIFY(3, "撤销申请通知"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家撤回任务通知,商家撤回已发布任务 → 通知已接单飞手。
|
||||||
|
*/
|
||||||
|
TASK_RECALL(4, "任务撤回通知");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 biz_type 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
WxNotifyBizTypeEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static WxNotifyBizTypeEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (WxNotifyBizTypeEnum type : values()) {
|
||||||
|
if (type.code.equals(code)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信通知发送任务状态枚举,对应 {@code wx_notify_task.status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum WxNotifyTaskStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待处理,入队后等待消费者取走。
|
||||||
|
*/
|
||||||
|
PENDING(0, "待处理"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理中,消费者已 CAS 占位,正在发送。
|
||||||
|
*/
|
||||||
|
PROCESSING(1, "处理中"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功,发送成功或跳过,消费完成后软删除。
|
||||||
|
*/
|
||||||
|
SUCCESS(2, "成功"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败,重试次数用尽,消费完成后软删除。
|
||||||
|
*/
|
||||||
|
FAILED(3, "失败");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
WxNotifyTaskStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static WxNotifyTaskStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (WxNotifyTaskStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信模板消息发送状态枚举,对应 {@code wx_message_log.send_status} 字段。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum WxSendStatusEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已跳过,接收人未关注服务号、模板配置缺失等,未实际调微信。
|
||||||
|
*/
|
||||||
|
SKIPPED(0, "已跳过"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已提交,微信受理成功,等待送达回调。
|
||||||
|
*/
|
||||||
|
SUBMITTED(1, "已提交"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送达成功,微信回调 success。
|
||||||
|
*/
|
||||||
|
DELIVERED(2, "送达成功"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送达失败,提交被拒或回调返回 failed:*。
|
||||||
|
*/
|
||||||
|
FAILED(3, "送达失败");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 send_status 列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
WxSendStatusEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static WxSendStatusEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (WxSendStatusEnum status : values()) {
|
||||||
|
if (status.code.equals(code)) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/main/java/com/haisen/laop/common/enums/YesNoEnum.java
Normal file
52
src/main/java/com/haisen/laop/common/enums/YesNoEnum.java
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.haisen.laop.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用"是否"标记枚举,对应数据库中 0/1 语义的 SMALLINT 标记列。
|
||||||
|
*
|
||||||
|
* <p>适用于纯布尔语义的标记字段(如 {@code task.is_premium}、{@code course.featured}、
|
||||||
|
* {@code task_record.insurance_verified}、{@code user.is_pilot_bound} 等)。
|
||||||
|
* 各业务表状态机字段(如任务状态、认证状态)禁止复用本枚举,必须定义领域专属状态枚举。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum YesNoEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 否。
|
||||||
|
*/
|
||||||
|
NO(0, "否"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是。
|
||||||
|
*/
|
||||||
|
YES(1, "是");
|
||||||
|
|
||||||
|
/** 数字编码,落库到 0/1 标记列。 */
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
/** 展示名称。 */
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
YesNoEnum(Integer code, String desc) {
|
||||||
|
this.code = code;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据编码获取枚举实例,找不到返回 null。
|
||||||
|
*/
|
||||||
|
public static YesNoEnum of(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (YesNoEnum value : values()) {
|
||||||
|
if (value.code.equals(code)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/main/java/com/haisen/laop/config/JacksonConfig.java
Normal file
84
src/main/java/com/haisen/laop/config/JacksonConfig.java
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.haisen.laop.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson 全局序列化/反序列化配置。
|
||||||
|
*
|
||||||
|
* <p><b>为什么需要此配置:</b>Spring Boot 3 默认 Jackson 对 {@link LocalDateTime} 使用
|
||||||
|
* ISO-8601 格式({@code 2026-08-27T10:00:00}),但前端(小程序/H5)通常以可读的
|
||||||
|
* {@code yyyy-MM-dd HH:mm} 格式提交和展示时间。此配置统一覆盖,前后端契约一致。</p>
|
||||||
|
*
|
||||||
|
* <p><b>影响范围:</b>所有 Controller 的 {@code @RequestBody LocalDateTime} 反序列化
|
||||||
|
* 和 {@code @ResponseBody LocalDateTime} 序列化均走此格式。DB 层 JPA/Hibernate
|
||||||
|
* 不受影响(JDBC 直接处理 {@link LocalDateTime} 对象)。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
/** 日期时间格式:前端提交和后端响应统一走此格式。 */
|
||||||
|
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
|
/** 日期格式。 */
|
||||||
|
public static final String DATE_PATTERN = "yyyy-MM-dd";
|
||||||
|
|
||||||
|
/** 时间格式。 */
|
||||||
|
public static final String TIME_PATTERN = "HH:mm:ss";
|
||||||
|
|
||||||
|
/** 秒级精度的日期时间(用于发布任务等场景)。 */
|
||||||
|
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
|
||||||
|
|
||||||
|
/** 日期格式化器。 */
|
||||||
|
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
|
||||||
|
|
||||||
|
/** 时间格式化器。 */
|
||||||
|
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 {@link ObjectMapper},覆盖 Spring Boot 自动配置。
|
||||||
|
*
|
||||||
|
* <p>注册 {@link JavaTimeModule} 并替换 Java 8 时间类型的序列化/反序列化格式。</p>
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
|
||||||
|
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||||
|
|
||||||
|
// 反序列化:前端 → Java 对象
|
||||||
|
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
|
||||||
|
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
|
||||||
|
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
|
||||||
|
|
||||||
|
// 序列化:Java 对象 → 前端
|
||||||
|
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
|
||||||
|
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DATE_FORMATTER));
|
||||||
|
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(TIME_FORMATTER));
|
||||||
|
|
||||||
|
ObjectMapper mapper = builder.modules(javaTimeModule).build();
|
||||||
|
// 禁止序列化时把 Java 8 时间类型写成数组([2026,8,27,10,0,0])
|
||||||
|
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
log.info("[JacksonConfig] LocalDateTime 格式统一为 {}", DATE_TIME_PATTERN);
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.haisen.laop.config;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户上下文拦截器:从 {@code Authorization} 请求头解析当前用户 ID 并写入 {@link RequestContext},
|
||||||
|
* 供业务代码通过 {@code RequestContext.getUserId()} 获取"当前登录用户"。
|
||||||
|
*
|
||||||
|
* <p><b>这不是权限校验:</b>本拦截器不做任何登录态/角色校验,也从不拦截请求(始终放行)。
|
||||||
|
* 项目当前的登录标识约定为"token 即 userId",管理端与小程序端均无权限控制需求;
|
||||||
|
* 后续若升级为 JWT,仅需在本类内部替换解析逻辑,调用点无需改动。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class UserContextInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization header 名称。
|
||||||
|
*/
|
||||||
|
private static final String AUTH_HEADER = "Authorization";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bearer token 前缀。
|
||||||
|
*/
|
||||||
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||||
|
String auth = request.getHeader(AUTH_HEADER);
|
||||||
|
if (StringUtils.isBlank(auth)) {
|
||||||
|
// 未携带登录标识,放行;需要当前用户的接口自行通过 RequestContext.getUserId() 判断
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String token = auth.startsWith(BEARER_PREFIX) ? auth.substring(BEARER_PREFIX.length()).trim() : auth.trim();
|
||||||
|
if (StringUtils.isNotBlank(token)) {
|
||||||
|
RequestContext.setUserId(Long.parseLong(token));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
// 登录标识不是合法数字,视为未登录,放行由业务层处理
|
||||||
|
log.warn("[用户上下文] 登录标识解析失败: {}", auth);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Object handler, Exception ex) {
|
||||||
|
// 请求结束清理 ThreadLocal,防止线程复用时用户信息串线
|
||||||
|
RequestContext.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/main/java/com/haisen/laop/config/WebConfig.java
Normal file
44
src/main/java/com/haisen/laop/config/WebConfig.java
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.haisen.laop.config;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web MVC 统一配置:静态资源映射 + 全局拦截器注册。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
private final UserContextInterceptor userContextInterceptor;
|
||||||
|
|
||||||
|
/** 静态资源根目录绝对路径,末尾不带斜杠(dev/prod 各自配置 laop.static-dir) */
|
||||||
|
@Value("${laop.static-dir}")
|
||||||
|
private String staticDir;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/static/**")
|
||||||
|
.addResourceLocations("file:" + staticDir + "/")
|
||||||
|
.setCachePeriod(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
// 用户上下文拦截器:解析 Authorization 头中的当前用户 ID 供业务层使用,
|
||||||
|
// 不做任何权限校验,始终放行
|
||||||
|
registry.addInterceptor(userContextInterceptor)
|
||||||
|
.addPathPatterns("/**")
|
||||||
|
.excludePathPatterns(
|
||||||
|
"/static/**",
|
||||||
|
"/error",
|
||||||
|
"/favicon.ico"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.AnnouncementQueryDTO;
|
||||||
|
import com.haisen.laop.dto.AnnouncementSaveDTO;
|
||||||
|
import com.haisen.laop.service.AnnouncementService;
|
||||||
|
import com.haisen.laop.vo.AnnouncementVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告模块 —— Web 后台管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台(管理员电脑端操作)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /announcement/*},与小程序端公告查询(待扩展)严格隔离。
|
||||||
|
* <p><b>职责:</b>承载通知公告的维护操作(分页查询、详情、新增、编辑、显示/隐藏、删除),
|
||||||
|
* 业务逻辑全部委托 {@link AnnouncementService},本层只做参数接收与 Result 包装。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/announcement")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AnnouncementController {
|
||||||
|
|
||||||
|
private final AnnouncementService announcementService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告分页列表。
|
||||||
|
*
|
||||||
|
* <p>返回结构与小程序端分页约定保持一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。</p>
|
||||||
|
*
|
||||||
|
* @param query 筛选条件(GET 参数自动绑定,全部可选)
|
||||||
|
* @return 公告 VO 分页数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(AnnouncementQueryDTO query) {
|
||||||
|
Page<AnnouncementVO> page = announcementService.page(query);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告详情,用于编辑回显。
|
||||||
|
*
|
||||||
|
* @param id 公告 ID
|
||||||
|
* @return 公告 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<AnnouncementVO> detail(@PathVariable Long id) {
|
||||||
|
return Result.success(announcementService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增公告。
|
||||||
|
*
|
||||||
|
* @param dto 公告表单全部字段
|
||||||
|
* @return 新增公告的 ID
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public Result<Long> create(@Valid @RequestBody AnnouncementSaveDTO dto) {
|
||||||
|
return Result.success(announcementService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑公告(全量更新)。
|
||||||
|
*
|
||||||
|
* @param id 公告 ID
|
||||||
|
* @param dto 公告表单全部字段
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody AnnouncementSaveDTO dto) {
|
||||||
|
announcementService.update(id, dto);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除公告(软删除)。
|
||||||
|
*
|
||||||
|
* @param id 公告 ID
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Result<Void> remove(@PathVariable Long id) {
|
||||||
|
announcementService.delete(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换公告展示状态(显示/隐藏)。
|
||||||
|
*
|
||||||
|
* @param id 公告 ID
|
||||||
|
* @param status 目标状态:0隐藏 1显示
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/status")
|
||||||
|
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
announcementService.updateStatus(id, status);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.AppBannerQueryDTO;
|
||||||
|
import com.haisen.laop.dto.AppBannerSaveDTO;
|
||||||
|
import com.haisen.laop.service.AppBannerService;
|
||||||
|
import com.haisen.laop.vo.AppBannerVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图模块 —— Web 后台管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台(管理员电脑端操作)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /banner/*},与小程序端展示接口(待扩展)严格隔离。
|
||||||
|
* <p><b>职责:</b>承载首页轮播图的维护操作(分页查询、详情、新增、编辑、排序调整、启停、删除),
|
||||||
|
* 业务逻辑全部委托 {@link AppBannerService},本层只做参数接收与 Result 包装。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/banner")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppBannerController {
|
||||||
|
|
||||||
|
private final AppBannerService appBannerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图分页列表。
|
||||||
|
*
|
||||||
|
* <p>返回结构与项目其他分页约定保持一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。</p>
|
||||||
|
*
|
||||||
|
* @param query 筛选条件(GET 参数自动绑定,全部可选)
|
||||||
|
* @return 轮播图 VO 分页数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(AppBannerQueryDTO query) {
|
||||||
|
Page<AppBannerVO> page = appBannerService.page(query);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图详情,用于编辑回显。
|
||||||
|
*
|
||||||
|
* @param id 轮播图 ID
|
||||||
|
* @return 轮播图 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<AppBannerVO> detail(@PathVariable Long id) {
|
||||||
|
return Result.success(appBannerService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增轮播图。
|
||||||
|
*
|
||||||
|
* @param dto 轮播图表单全部字段
|
||||||
|
* @return 新增轮播图的 ID
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public Result<Long> create(@Valid @RequestBody AppBannerSaveDTO dto) {
|
||||||
|
return Result.success(appBannerService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑轮播图(全量更新)。
|
||||||
|
*
|
||||||
|
* @param id 轮播图 ID
|
||||||
|
* @param dto 轮播图表单全部字段
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody AppBannerSaveDTO dto) {
|
||||||
|
appBannerService.update(id, dto);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除轮播图(软删除)。
|
||||||
|
*
|
||||||
|
* @param id 轮播图 ID
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Result<Void> remove(@PathVariable Long id) {
|
||||||
|
appBannerService.delete(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换轮播图展示状态(显示/隐藏)。
|
||||||
|
*
|
||||||
|
* @param id 轮播图 ID
|
||||||
|
* @param status 目标状态:0隐藏 1显示
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/status")
|
||||||
|
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
appBannerService.updateStatus(id, status);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整轮播图排序值。
|
||||||
|
*
|
||||||
|
* @param id 轮播图 ID
|
||||||
|
* @param sortOrder 目标排序值
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/sort")
|
||||||
|
public Result<Void> updateSort(@PathVariable Long id, @RequestParam Integer sortOrder) {
|
||||||
|
appBannerService.updateSort(id, sortOrder);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/main/java/com/haisen/laop/controller/CourseController.java
Normal file
152
src/main/java/com/haisen/laop/controller/CourseController.java
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.CourseQueryDTO;
|
||||||
|
import com.haisen.laop.dto.CourseSaveDTO;
|
||||||
|
import com.haisen.laop.service.CourseService;
|
||||||
|
import com.haisen.laop.vo.AdminCourseAppointmentVO;
|
||||||
|
import com.haisen.laop.vo.CourseVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程模块 —— Web 后台管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台(管理员电脑端操作)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /course/*},与小程序端 {@code /api/mini/course/*}(待扩展)严格隔离。
|
||||||
|
* <p><b>职责:</b>承载研学课程的维护操作(分页查询、详情、新增、编辑、上下架、删除),
|
||||||
|
* 业务逻辑全部委托 {@link CourseService},本层只做参数接收与 Result 包装。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/course")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CourseController {
|
||||||
|
|
||||||
|
private final CourseService courseService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程分页列表。
|
||||||
|
*
|
||||||
|
* <p>返回结构与小程序端分页约定保持一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。</p>
|
||||||
|
*
|
||||||
|
* @param query 筛选条件(GET 参数自动绑定,全部可选)
|
||||||
|
* @return 课程 VO 分页数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(CourseQueryDTO query) {
|
||||||
|
Page<CourseVO> page = courseService.page(query);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程详情,用于编辑回显。
|
||||||
|
*
|
||||||
|
* @param id 课程 ID
|
||||||
|
* @return 课程 VO(含宣传图绝对 URL 与富文本简介)
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<CourseVO> detail(@PathVariable Long id) {
|
||||||
|
return Result.success(courseService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增课程。
|
||||||
|
*
|
||||||
|
* @param dto 课程表单全部字段
|
||||||
|
* @return 新增课程的 ID
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public Result<Long> create(@Valid @RequestBody CourseSaveDTO dto) {
|
||||||
|
return Result.success(courseService.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑课程(全量更新)。
|
||||||
|
*
|
||||||
|
* @param id 课程 ID
|
||||||
|
* @param dto 课程表单全部字段
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody CourseSaveDTO dto) {
|
||||||
|
courseService.update(id, dto);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除课程(软删除)。
|
||||||
|
*
|
||||||
|
* @param id 课程 ID
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Result<Void> remove(@PathVariable Long id) {
|
||||||
|
courseService.delete(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程上下架。
|
||||||
|
*
|
||||||
|
* @param id 课程 ID
|
||||||
|
* @param status 目标状态:0下架 1上架
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/status")
|
||||||
|
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
courseService.updateStatus(id, status);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程预约名单(分页):管理端某课程下的全部预约记录。
|
||||||
|
*
|
||||||
|
* <p>返回结构与其他分页接口一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。预约记录按创建时间倒序(最新预约在最前)。</p>
|
||||||
|
*
|
||||||
|
* @param courseId 课程 ID(必传,查哪门课的预约)
|
||||||
|
* @param pageNo 页码(0 基),前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @return 管理端预约 VO 分页(含预约人昵称/手机号与课程名称)
|
||||||
|
*/
|
||||||
|
@GetMapping("/appointments")
|
||||||
|
public Result<Map<String, Object>> appointments(
|
||||||
|
@RequestParam Long courseId,
|
||||||
|
@RequestParam int pageNo,
|
||||||
|
@RequestParam int pageSize) {
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize);
|
||||||
|
Page<AdminCourseAppointmentVO> page = courseService.listAppointmentsByCourse(courseId, pageable);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.service.DashboardService;
|
||||||
|
import com.haisen.laop.vo.DashboardVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据驾驶舱 —— Web 后台首页统计聚合 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台 Dashboard 首页。
|
||||||
|
* <p><b>路径前缀:</b>{@code /dashboard/*},单接口聚合返回全部统计数据,避免多接口瀑布加载。
|
||||||
|
* <p><b>职责:</b>Controller 层只做路由与 Result 包装,业务逻辑全部委托 {@link DashboardService}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/dashboard")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DashboardController {
|
||||||
|
|
||||||
|
private final DashboardService dashboardService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据驾驶舱全量统计数据。
|
||||||
|
*
|
||||||
|
* <p>一次性返回用户/商家/飞手总量、任务总量与状态分布、近 6 个月月度增长趋势、
|
||||||
|
* 任务类型分布等所有 Dashboard 图表所需数据。</p>
|
||||||
|
*
|
||||||
|
* @return Dashboard 聚合 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/stats")
|
||||||
|
public Result<DashboardVO> getStats() {
|
||||||
|
return Result.success(dashboardService.getDashboardStats());
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/main/java/com/haisen/laop/controller/ExamController.java
Normal file
124
src/main/java/com/haisen/laop/controller/ExamController.java
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.ExamQuestionSaveDTO;
|
||||||
|
import com.haisen.laop.dto.TrainingStoreDTO;
|
||||||
|
import com.haisen.laop.service.ExamService;
|
||||||
|
import com.haisen.laop.vo.ExamBankVO;
|
||||||
|
import com.haisen.laop.vo.ExamQuestionVO;
|
||||||
|
import com.haisen.laop.vo.TrainingStoreVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 培训考试模块 —— Web 管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台。
|
||||||
|
* <p><b>路径前缀:</b>{@code /exam/*}。
|
||||||
|
* <p><b>职责:</b>仅负责接收请求参数、调用 Service、返回 Result,
|
||||||
|
* 业务逻辑全部委托 {@link ExamService}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/exam")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExamController {
|
||||||
|
|
||||||
|
private final ExamService examService;
|
||||||
|
|
||||||
|
// ==================== 培训 tab ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取门店培训信息(培训 tab)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/training")
|
||||||
|
public Result<TrainingStoreVO> getTrainingStore() {
|
||||||
|
return Result.success(examService.getTrainingStore());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑门店培训信息(管理员操作)。
|
||||||
|
*/
|
||||||
|
@PutMapping("/training")
|
||||||
|
public Result<Void> saveTrainingStore(@RequestBody TrainingStoreDTO dto) {
|
||||||
|
examService.saveTrainingStore(dto);
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 题库 tab — 题库管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有有效题库列表(题库 tab 下拉筛选用)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/banks")
|
||||||
|
public Result<List<ExamBankVO>> listBanks() {
|
||||||
|
return Result.success(examService.listBanks());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 题库 tab — 题目 CRUD ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端分页查询题目,支持按题库/题型/关键词筛选,返回完整字段(含答案、解析)。
|
||||||
|
*
|
||||||
|
* @param bankId 题库ID(可选)
|
||||||
|
* @param questionType 题型(可选):1单选 2多选 3判断
|
||||||
|
* @param keyword 关键词(题干模糊匹配,可选)
|
||||||
|
* @param pageNo 页码(0 基)
|
||||||
|
* @param pageSize 每页条数
|
||||||
|
*/
|
||||||
|
@GetMapping("/questions")
|
||||||
|
public Result<Map<String, Object>> listQuestions(
|
||||||
|
@RequestParam(required = false) Long bankId,
|
||||||
|
@RequestParam(required = false) Integer questionType,
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int pageNo,
|
||||||
|
@RequestParam(defaultValue = "20") int pageSize) {
|
||||||
|
List<ExamQuestionVO> list = examService.listQuestionsForAdmin(bankId, questionType, keyword, pageNo, pageSize);
|
||||||
|
long total = examService.countQuestionsForAdmin(bankId, questionType, keyword);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", list);
|
||||||
|
data.put("total", total);
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增题目。
|
||||||
|
*/
|
||||||
|
@PostMapping("/questions")
|
||||||
|
public Result<ExamQuestionVO> createQuestion(@Valid @RequestBody ExamQuestionSaveDTO dto) {
|
||||||
|
return Result.success(examService.createQuestion(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑题目。
|
||||||
|
*/
|
||||||
|
@PutMapping("/questions/{id}")
|
||||||
|
public Result<ExamQuestionVO> updateQuestion(@PathVariable Long id,
|
||||||
|
@Valid @RequestBody ExamQuestionSaveDTO dto) {
|
||||||
|
dto.setId(id);
|
||||||
|
return Result.success(examService.updateQuestion(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除题目。
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/questions/{id}")
|
||||||
|
public Result<Void> deleteQuestion(@PathVariable Long id) {
|
||||||
|
examService.deleteQuestion(id);
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/main/java/com/haisen/laop/controller/FileController.java
Normal file
59
src/main/java/com/haisen/laop/controller/FileController.java
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.common.enums.FileTypeEnum;
|
||||||
|
import com.haisen.laop.service.FileService;
|
||||||
|
import com.haisen.laop.vo.FileUploadVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件模块 Controller,聚焦单文件上传能力。
|
||||||
|
*
|
||||||
|
* <p><b>上传协议:</b>HTTP multipart/form-data,文件字段名固定为 {@code file},
|
||||||
|
* 业务类型通过 {@code fileType} 表单参数传入(如 {@code SCENE_IMAGE})。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/file")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileController {
|
||||||
|
|
||||||
|
private final FileService fileService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单文件上传。
|
||||||
|
*
|
||||||
|
* <p>请求头需携带 {@code Authorization: Bearer {userId}},
|
||||||
|
* 未登录时 uploaderId 为 null(后续可在 FileService 中按需强制校验)。</p>
|
||||||
|
*
|
||||||
|
* @param file 上传的文件,multipart/form-data 字段名 {@code file}
|
||||||
|
* @param fileType 文件类型字符串标识,如 {@code SCENE_IMAGE}、{@code AVATAR}
|
||||||
|
* @return 文件元信息(fileId + fileUrl + fileSize + fileSuffix)
|
||||||
|
*/
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public Result<FileUploadVO> upload(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam("fileType") String fileType) {
|
||||||
|
// fileType 枚举合法性校验,防止非法字符串透传到 Service/存储层
|
||||||
|
if (!FileTypeEnum.isValid(fileType)) {
|
||||||
|
throw new BusinessException("不支持的文件类型:" + fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Long uploaderId = RequestContext.getUserId();
|
||||||
|
if (uploaderId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
FileUploadVO vo = fileService.upload(uploaderId, file, fileType);
|
||||||
|
return Result.success(vo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.entity.AppConfig;
|
||||||
|
import com.haisen.laop.service.AppConfigService;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序功能配置 Controller,承载"消息推送开关、积分规则参数等运营功能配置"的读写。
|
||||||
|
*
|
||||||
|
* <p><b>路径前缀:</b>{@code /mini/config/*}
|
||||||
|
* <b>消费端:</b>Web 管理端(小程序管理 → 功能配置)。
|
||||||
|
* <b>职责:</b>配置项列表查询 + 单个配置项的值更新,业务逻辑全部委托 {@link AppConfigService}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/mini/config")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniConfigController {
|
||||||
|
|
||||||
|
private final AppConfigService appConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全部功能配置项列表(消息推送开关、积分规则参数等)。
|
||||||
|
*
|
||||||
|
* @return AppConfig 列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<List<AppConfig>> list() {
|
||||||
|
return Result.success(appConfigService.listAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定配置项的值。
|
||||||
|
*
|
||||||
|
* @param configKey 配置键,如 'mp_push_enabled'
|
||||||
|
* @param dto 请求体,只包含 configValue 字段('0' 或 '1')
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PutMapping("/{configKey}")
|
||||||
|
public Result<Void> update(
|
||||||
|
@PathVariable String configKey,
|
||||||
|
@RequestBody ConfigUpdateDTO dto) {
|
||||||
|
appConfigService.updateValue(configKey, dto.getConfigValue(), null);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置项更新 DTO,仅包含 configValue 一个字段。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class ConfigUpdateDTO {
|
||||||
|
/** 新配置值,布尔开关传 '0' 或 '1'。 */
|
||||||
|
private String configValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/main/java/com/haisen/laop/controller/TaskController.java
Normal file
108
src/main/java/com/haisen/laop/controller/TaskController.java
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.TaskManageQueryDTO;
|
||||||
|
import com.haisen.laop.service.TaskService;
|
||||||
|
import com.haisen.laop.vo.TaskManageVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务模块 —— Web 后台管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台(管理员电脑端操作)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /task/*},不带 /api 前缀,与小程序端 /api/mini/task/* 严格隔离。
|
||||||
|
* <p><b>职责:</b>承载任务管理类操作(列表查询、详情查看、取消、软删除),
|
||||||
|
* 不包含任务发布(发布由 {@link com.haisen.laop.controller.mini.MiniTaskController} 承载)。</p>
|
||||||
|
*
|
||||||
|
* <p>与小程序端共享同一个 {@link TaskService},Controller 层仅做入口隔离 + 参数接收 + 权限注入。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/task")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TaskController {
|
||||||
|
|
||||||
|
private final TaskService taskService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务分页列表(Web 管理端):全量任务,支持关键词/类型/状态筛选。
|
||||||
|
*
|
||||||
|
* <p>返回结构与小程序端分页约定保持一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。</p>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基)
|
||||||
|
* @param pageSize 每页条数
|
||||||
|
* @param query 筛选条件(GET 参数自动绑定,全部可选)
|
||||||
|
* @return 任务管理 VO 分页数据(含发布商家简要信息)
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(
|
||||||
|
@RequestParam(defaultValue = "0") int pageNo,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
TaskManageQueryDTO query) {
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize,
|
||||||
|
Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||||
|
Page<TaskManageVO> page = taskService.managePage(query, pageable);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务详情(Web 管理端):全量任务信息 + 发布商家信息 + 场景图 URL。
|
||||||
|
*
|
||||||
|
* @param id 任务 ID
|
||||||
|
* @return 任务管理 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<TaskManageVO> detail(@PathVariable Long id) {
|
||||||
|
return Result.success(taskService.manageDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消任务(管理员干预),将状态置为"已取消"。
|
||||||
|
*
|
||||||
|
* <p>仅允许从待接单/已接单/执行中状态取消;已完成或已取消的任务不允许再次取消。</p>
|
||||||
|
*
|
||||||
|
* @param id 任务 ID
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/cancel")
|
||||||
|
public Result<Void> cancel(@PathVariable Long id) {
|
||||||
|
taskService.cancel(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除任务(管理员干预),写入 deleted_at。
|
||||||
|
*
|
||||||
|
* <p>仅允许删除草稿/已完成/已取消的任务;进行中的任务需先取消。</p>
|
||||||
|
*
|
||||||
|
* @param id 任务 ID
|
||||||
|
* @return 空数据成功响应
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public Result<Void> remove(@PathVariable Long id) {
|
||||||
|
taskService.delete(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/main/java/com/haisen/laop/controller/UserController.java
Normal file
155
src/main/java/com/haisen/laop/controller/UserController.java
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.PointsAdjustDTO;
|
||||||
|
import com.haisen.laop.dto.UserQueryDTO;
|
||||||
|
import com.haisen.laop.service.UserService;
|
||||||
|
import com.haisen.laop.vo.UserManageVO;
|
||||||
|
import com.haisen.laop.vo.UserMerchantDetailVO;
|
||||||
|
import com.haisen.laop.vo.UserPilotDetailVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户模块 —— Web 后台管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台(管理员电脑端操作)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /user/*},不带 /api 前缀,与小程序端用户接口严格隔离。
|
||||||
|
* <p><b>职责:</b>仅负责接收请求参数、调用 Service、返回 Result,
|
||||||
|
* 业务逻辑全部委托 {@link UserService},本层禁止包含任何业务逻辑或 Repository 调用。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户分页列表(Web 管理端):全量注册用户 + 关键词筛选 + 用户类型(商家/飞手)。
|
||||||
|
*
|
||||||
|
* <p>返回结构与小程序端分页约定保持一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。</p>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基)
|
||||||
|
* @param pageSize 每页条数
|
||||||
|
* @param query 筛选条件(GET 参数自动绑定,全部可选)
|
||||||
|
* @return 用户管理 VO 分页数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(
|
||||||
|
@RequestParam(defaultValue = "0") int pageNo,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize,
|
||||||
|
UserQueryDTO query) {
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize,
|
||||||
|
Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||||
|
Page<UserManageVO> page = userService.pageUsers(query, pageable);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家详情弹窗:cert 信息 + 最近发布的任务列表。
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 商家详情包装 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/{userId}/merchant")
|
||||||
|
public Result<UserMerchantDetailVO> merchantDetail(@PathVariable Long userId) {
|
||||||
|
return Result.success(userService.getMerchantDetail(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手详情弹窗:cert 信息 + 最近接单的任务列表。
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 飞手详情包装 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/{userId}/pilot")
|
||||||
|
public Result<UserPilotDetailVO> pilotDetail(@PathVariable Long userId) {
|
||||||
|
return Result.success(userService.getPilotDetail(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制取消商家认证:软删除 merchant 记录 + 重置 user 绑定标记。
|
||||||
|
*
|
||||||
|
* <p><b>前置校验:</b>若该商家存在"待接单/已接单/执行中"的任务,则拒绝取消。</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 操作成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/{userId}/revoke-merchant")
|
||||||
|
public Result<Void> revokeMerchant(@PathVariable Long userId) {
|
||||||
|
userService.revokeMerchantCert(userId);
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制取消飞手认证:软删除 pilot 记录 + 重置 user 绑定标记。
|
||||||
|
*
|
||||||
|
* <p><b>前置校验:</b>若该飞手存在"待接单/已接单/执行中"的任务,则拒绝取消。</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @return 操作成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/{userId}/revoke-pilot")
|
||||||
|
public Result<Void> revokePilot(@PathVariable Long userId) {
|
||||||
|
userService.revokePilotCert(userId);
|
||||||
|
return Result.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户积分明细弹窗:分页查询指定用户的积分增减流水,按时间倒序。
|
||||||
|
*
|
||||||
|
* <p>业务逻辑(Repository 调用 + PO→VO 转换 + 分页封装)全部委托 UserService。</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param pageNo 页码(0 基),默认 0
|
||||||
|
* @param pageSize 每页条数,默认 10
|
||||||
|
* @return 积分明细分页数据
|
||||||
|
*/
|
||||||
|
@GetMapping("/{userId}/points")
|
||||||
|
public Result<Map<String, Object>> pointsDetail(
|
||||||
|
@PathVariable Long userId,
|
||||||
|
@RequestParam(defaultValue = "0") int pageNo,
|
||||||
|
@RequestParam(defaultValue = "10") int pageSize) {
|
||||||
|
return Result.success(userService.pointsDetail(userId, pageNo, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动增减用户积分:管理端在"积分明细"弹窗中对指定用户执行人工新增/扣减。
|
||||||
|
*
|
||||||
|
* <p>扣减时余额不足由 Service 层拒绝;调整成功返回最新余额,供前端即时刷新弹窗与列表。</p>
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param dto 调整请求(变动类型 1新增 2扣减、积分数量、调整原因)
|
||||||
|
* @return 调整后的最新积分余额
|
||||||
|
*/
|
||||||
|
@PostMapping("/{userId}/points/adjust")
|
||||||
|
public Result<Integer> adjustPoints(
|
||||||
|
@PathVariable Long userId,
|
||||||
|
@Valid @RequestBody PointsAdjustDTO dto) {
|
||||||
|
return Result.success(userService.adjustPoints(userId, dto));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.MessageLogQueryDTO;
|
||||||
|
import com.haisen.laop.service.WxMessageLogService;
|
||||||
|
import com.haisen.laop.vo.WxMessageLogVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信模板消息发送日志 —— Web 后台管理端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>Web 管理后台"消息推送日志"页面(只读)。
|
||||||
|
* <p><b>职责:</b>提供发送日志的分页查询能力,业务逻辑全部委托
|
||||||
|
* {@link WxMessageLogService},本层只做参数接收与 Result 包装。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/wx-message-log")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxMessageLogController {
|
||||||
|
|
||||||
|
private final WxMessageLogService wxMessageLogService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送日志分页列表,支持按接收人、业务类型、发送状态、openid 组合筛选。
|
||||||
|
*
|
||||||
|
* <p>返回结构与 Web 端分页约定保持一致:{list, totalElements, totalPages, pageNo, pageSize},
|
||||||
|
* 其中 pageNo 为 0 基页码。</p>
|
||||||
|
*
|
||||||
|
* @param query 筛选条件(GET 参数自动绑定,全部可选)
|
||||||
|
* @return 日志 VO 分页数据,按入库时间倒序
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(MessageLogQueryDTO query) {
|
||||||
|
Page<WxMessageLogVO> page = wxMessageLogService.page(query);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.haisen.laop.controller;
|
||||||
|
|
||||||
|
import com.haisen.laop.service.WxMpCallbackService;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信服务号回调入口 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>微信服务器(非人类用户)。在服务号后台"服务器配置"中填写
|
||||||
|
* {@code {域名}/api/wechat/mp/callback} 与 Token,微信会将服务器验证请求与事件消息
|
||||||
|
* (模板消息送达结果、关注/取关)推送到此。</p>
|
||||||
|
*
|
||||||
|
* <p><b>响应约定:</b>微信要求验证请求原样返回 echostr、事件请求应答 "success"
|
||||||
|
* 均为纯文本报文(非 JSON),故直接写 {@link HttpServletResponse},
|
||||||
|
* 避免统一响应结构包装后微信校验失败。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/wechat/mp")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxMpCallbackController {
|
||||||
|
|
||||||
|
/** 微信约定的成功应答报文 */
|
||||||
|
private static final String REPLY_SUCCESS = "success";
|
||||||
|
|
||||||
|
private final WxMpCallbackService wxMpCallbackService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器配置 URL 有效性验证(微信后台保存配置时触发一次)。
|
||||||
|
*
|
||||||
|
* <p>验证通过原样返回 echostr;失败返回 403(微信会提示配置失败)。</p>
|
||||||
|
*/
|
||||||
|
@GetMapping("/callback")
|
||||||
|
public void verify(@RequestParam(required = false) String signature,
|
||||||
|
@RequestParam(required = false) String timestamp,
|
||||||
|
@RequestParam(required = false) String nonce,
|
||||||
|
@RequestParam(required = false) String echostr,
|
||||||
|
HttpServletResponse response) throws IOException {
|
||||||
|
String echo = wxMpCallbackService.verifyUrl(signature, timestamp, nonce, echostr);
|
||||||
|
writePlain(response, echo, !StringUtils.isEmpty(echo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收微信推送的事件消息(模板消息送达结果、关注/取关等)。
|
||||||
|
*
|
||||||
|
* <p>签名校验失败返回 403 拒绝处理;业务处理异常时全局异常处理器兜底,
|
||||||
|
* 微信侧收不到 success 应答会按策略重推(日志表按 MsgID 幂等回写,重推无副作用)。</p>
|
||||||
|
*/
|
||||||
|
@PostMapping("/callback")
|
||||||
|
public void onEvent(@RequestParam(required = false) String signature,
|
||||||
|
@RequestParam(required = false) String timestamp,
|
||||||
|
@RequestParam(required = false) String nonce,
|
||||||
|
@RequestBody(required = false) String xmlBody,
|
||||||
|
HttpServletResponse response) throws IOException {
|
||||||
|
if (!wxMpCallbackService.verifySignature(signature, timestamp, nonce)) {
|
||||||
|
log.warn("[微信回调] 事件推送验签失败,拒绝处理");
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String reply = wxMpCallbackService.handleEventMessage(xmlBody);
|
||||||
|
writePlain(response, reply, StringUtils.isNotBlank(reply));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出纯文本响应(微信回调协议要求,不走统一 JSON 结构)。
|
||||||
|
*/
|
||||||
|
private void writePlain(HttpServletResponse response, String content, boolean ok) throws IOException {
|
||||||
|
if (!ok) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.setContentType("text/plain;charset=UTF-8");
|
||||||
|
response.getWriter().write(content);
|
||||||
|
response.getWriter().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.service.AnnouncementService;
|
||||||
|
import com.haisen.laop.vo.AnnouncementVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知公告模块 —— 小程序端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>微信小程序(首页公告条 + 公告详情页)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/announcement/*},与 Web 后台端 {@code /announcement/*} 严格隔离。
|
||||||
|
* <p><b>职责:</b>只读展示运营后台配置的公告;固定条件"显示 + 未软删 + 在生效窗口期内",
|
||||||
|
* 由 {@link AnnouncementService#listForMini} / {@link AnnouncementService#detailForMini} 统一保障。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/announcement")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniAnnouncementController {
|
||||||
|
|
||||||
|
private final AnnouncementService announcementService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页公告条:拉取最新可见公告列表,按创建时间倒序。
|
||||||
|
*
|
||||||
|
* <p>前端取列表第一条作为公告条内容,点击跳转 {@link #detail(Long)} 查看正文。</p>
|
||||||
|
*
|
||||||
|
* @param limit 期望条数,默认 5,上限 20(由 Service 夹紧)
|
||||||
|
* @return 可见公告 VO 列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<List<AnnouncementVO>> list(@RequestParam(defaultValue = "5") int limit) {
|
||||||
|
return Result.success(announcementService.listForMini(limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告详情:点击公告条进入详情页,展示标题 + 正文 + 发布时间。
|
||||||
|
*
|
||||||
|
* @param id 公告 ID
|
||||||
|
* @return 公告 VO
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<AnnouncementVO> detail(@PathVariable Long id) {
|
||||||
|
return Result.success(announcementService.detailForMini(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.common.enums.CertStatusEnum;
|
||||||
|
import com.haisen.laop.dto.WxLoginDTO;
|
||||||
|
import com.haisen.laop.entity.User;
|
||||||
|
import com.haisen.laop.repository.MerchantRepository;
|
||||||
|
import com.haisen.laop.repository.PilotRepository;
|
||||||
|
import com.haisen.laop.repository.UserRepository;
|
||||||
|
import com.haisen.laop.service.LoginService;
|
||||||
|
import com.haisen.laop.vo.LoginVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序端鉴权 Controller,承载微信登录入口。
|
||||||
|
*
|
||||||
|
* <p><b>登录流程:</b>小程序前端调 {@code wx.login()} → 拿 code → POST 本接口 →
|
||||||
|
* 后端 code2session 换 openid → 查/建 User → 返回 token。</p>
|
||||||
|
*
|
||||||
|
* @author haisen
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniAuthController {
|
||||||
|
|
||||||
|
private final LoginService loginService;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PilotRepository pilotRepository;
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序登录。
|
||||||
|
*
|
||||||
|
* @param loginDTO 前端 wx.login() 返回的 code
|
||||||
|
* @return token + userId + 是否新用户
|
||||||
|
*/
|
||||||
|
@PostMapping("/login")
|
||||||
|
public Result<LoginVO> login(@Valid @RequestBody WxLoginDTO loginDTO) {
|
||||||
|
LoginVO vo = loginService.wxLogin(loginDTO.getCode(), loginDTO.getInviteUserId());
|
||||||
|
return Result.success(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户的精简信息(积分、昵称、认证状态等),供前端 UI 展示。
|
||||||
|
*
|
||||||
|
* <p>认证状态随用户信息一并返回,避免"我的"页面额外发起两次认证查询请求;
|
||||||
|
* 前端据此在昵称下方渲染 飞手/商家 身份标签。</p>
|
||||||
|
*
|
||||||
|
* @return {userId, nickname, avatarUrl, points, roleType, pilotCertStatus, merchantCertStatus}
|
||||||
|
*/
|
||||||
|
@GetMapping("/me")
|
||||||
|
public Result<Map<String, Object>> me() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new BusinessException("用户不存在"));
|
||||||
|
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("userId", user.getId());
|
||||||
|
data.put("nickname", user.getNickname());
|
||||||
|
data.put("avatarUrl", user.getAvatarUrl());
|
||||||
|
data.put("points", user.getPoints() == null ? 0 : user.getPoints());
|
||||||
|
data.put("roleType", user.getRoleType());
|
||||||
|
|
||||||
|
// 认证状态:无认证记录时返回未认证编码,保证前端类型稳定
|
||||||
|
var pilot = pilotRepository.findByUserIdAndDeletedAtIsNull(userId);
|
||||||
|
var merchant = merchantRepository.findByUserIdAndDeletedAtIsNull(userId);
|
||||||
|
data.put("pilotCertStatus", pilot == null ? CertStatusEnum.NONE.getCode() : pilot.getCertStatus());
|
||||||
|
data.put("merchantCertStatus", merchant == null ? CertStatusEnum.NONE.getCode() : merchant.getCertStatus());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.service.AppBannerService;
|
||||||
|
import com.haisen.laop.vo.AppBannerVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序首页轮播图接口:获取已显示的轮播图列表。
|
||||||
|
*
|
||||||
|
* <p><b>公开访问:</b>首页轮播图内容无需登录即可浏览(与任务大厅一致),
|
||||||
|
* 登录相关动作由具体交互入口自行保障。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/home/banner")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniBannerController {
|
||||||
|
|
||||||
|
private final AppBannerService appBannerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取首页轮播图列表:仅返回已显示、未软删除的轮播图,按 sort_order 升序排列。
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<List<AppBannerVO>> list() {
|
||||||
|
return Result.success(appBannerService.listForMini());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.MerchantCertifyDTO;
|
||||||
|
import com.haisen.laop.dto.PilotCertifyDTO;
|
||||||
|
import com.haisen.laop.service.CertService;
|
||||||
|
import com.haisen.laop.vo.MerchantCertVO;
|
||||||
|
import com.haisen.laop.vo.PilotCertVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证模块 —— 小程序端 Controller,承载飞手/商家认证的提交与回显。
|
||||||
|
*
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/cert/*};提交人身份由 {@link RequestContext}
|
||||||
|
* 从 Authorization token 注入,前端不可伪造他人认证。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/cert")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniCertController {
|
||||||
|
|
||||||
|
private final CertService certService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交飞手认证(个人执照信息)。
|
||||||
|
*
|
||||||
|
* @param dto 飞手执照信息表单
|
||||||
|
* @return 提交后的认证信息(含最新认证状态)
|
||||||
|
*/
|
||||||
|
@PostMapping("/pilot")
|
||||||
|
public Result<PilotCertVO> submitPilot(@Valid @RequestBody PilotCertifyDTO dto) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(certService.submitPilot(userId, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询我的飞手认证信息(认证页回显,从未提交过时 certStatus = 0)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/pilot")
|
||||||
|
public Result<PilotCertVO> getPilotCert() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(certService.getPilotCert(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交商家认证(商家信息 + 营业执照)。
|
||||||
|
*
|
||||||
|
* @param dto 商家信息表单
|
||||||
|
* @return 提交后的认证信息(含最新认证状态)
|
||||||
|
*/
|
||||||
|
@PostMapping("/merchant")
|
||||||
|
public Result<MerchantCertVO> submitMerchant(@Valid @RequestBody MerchantCertifyDTO dto) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(certService.submitMerchant(userId, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询我的商家认证信息(认证页回显,从未提交过时 certStatus = 0)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/merchant")
|
||||||
|
public Result<MerchantCertVO> getMerchantCert() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(certService.getMerchantCert(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.common.constants.CourseConstants;
|
||||||
|
import com.haisen.laop.dto.CourseAppointmentDTO;
|
||||||
|
import com.haisen.laop.service.CourseService;
|
||||||
|
import com.haisen.laop.vo.CourseAppointmentVO;
|
||||||
|
import com.haisen.laop.vo.MiniCourseVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 研学课程模块 —— 小程序端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>微信小程序(研学课程 Tab + 课程详情页 + 我的预约)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/course/*},与 Web 后台端 {@code /course/*} 严格隔离。
|
||||||
|
* <p><b>职责:</b>承载面向 C 端用户的课程浏览与预约操作,业务逻辑全部委托
|
||||||
|
* {@link CourseService},本层只做入口隔离 + 参数接收 + 鉴权注入。</p>
|
||||||
|
*
|
||||||
|
* <p><b>鉴权策略:</b>课程列表与详情公开访问(无需登录即可浏览);
|
||||||
|
* 提交预约与我的预约需登录,身份由 {@link RequestContext} 从 token 注入。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/course")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniCourseController {
|
||||||
|
|
||||||
|
private final CourseService courseService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程列表(分页):仅返回已上架课程,按创建时间倒序。
|
||||||
|
*
|
||||||
|
* <p>支持名称模糊 + 分类精确筛选(全部可选,不传即查全量上架课程)。</p>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基),前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @param name 课程名称(模糊,可选)
|
||||||
|
* @param category 分类编码(1认知启蒙 2技能进阶 3创新应用 4竞赛辅导,可选)
|
||||||
|
* @return {list, totalElements, totalPages, pageNo, pageSize}
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<Map<String, Object>> list(
|
||||||
|
@RequestParam int pageNo,
|
||||||
|
@RequestParam int pageSize,
|
||||||
|
@RequestParam(required = false) String name,
|
||||||
|
@RequestParam(required = false) Integer category) {
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize);
|
||||||
|
Page<MiniCourseVO> page = courseService.listForMini(name, category, pageable);
|
||||||
|
return Result.success(buildPageData(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程详情:点击列表卡片进入详情页,展示课程全部信息 + 图集 + 商家名称。
|
||||||
|
*
|
||||||
|
* @param id 课程 ID
|
||||||
|
* @return 小程序课程 VO(含图集绝对 URL 与富文本简介)
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Result<MiniCourseVO> detail(@PathVariable Long id) {
|
||||||
|
return Result.success(courseService.detailForMini(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精选课程列表:仅供小程序首页"精选研学课程"区块调用,轻量接口不做分页。
|
||||||
|
*
|
||||||
|
* <p>固定条件:上架 + 精选 + 未软删;按创建时间倒序,默认返回
|
||||||
|
* {@link CourseConstants#FEATURED_LIST_LIMIT} 条。</p>
|
||||||
|
*
|
||||||
|
* @param limit 最大条数(可选,默认 6,上限 50)
|
||||||
|
* @return 小程序课程 VO 列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/featured")
|
||||||
|
public Result<List<MiniCourseVO>> featured(
|
||||||
|
@RequestParam(required = false, defaultValue = "6") int limit) {
|
||||||
|
return Result.success(courseService.listFeatured(limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交课程预约。
|
||||||
|
*
|
||||||
|
* <p>请求头需携带 {@code Authorization: Bearer {userId}},
|
||||||
|
* 提交人身份由 {@link RequestContext} 自动注入;金额由后端按
|
||||||
|
* {@code course.price × personCount} 计算,前端不传金额。</p>
|
||||||
|
*
|
||||||
|
* @param dto 预约表单(课程ID、预约时间、人数、联系人、联系电话、备注)
|
||||||
|
* @return 新增预约 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/appointment")
|
||||||
|
public Result<Long> createAppointment(@Valid @RequestBody CourseAppointmentDTO dto) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(courseService.createAppointment(userId, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的预约(分页):当前用户提交的全部课程预约,按创建时间倒序。
|
||||||
|
*
|
||||||
|
* <p>组装课程名称与宣传图绝对 URL,避免前端再发课程详情请求。</p>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基),前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @return {list, totalElements, totalPages, pageNo, pageSize}
|
||||||
|
*/
|
||||||
|
@GetMapping("/appointments")
|
||||||
|
public Result<Map<String, Object>> myAppointments(
|
||||||
|
@RequestParam int pageNo,
|
||||||
|
@RequestParam int pageSize) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize);
|
||||||
|
Page<CourseAppointmentVO> page = courseService.myAppointments(userId, pageable);
|
||||||
|
return Result.success(buildPageData(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消我的预约:用户自助取消自己的"已预约"状态预约。
|
||||||
|
*
|
||||||
|
* <p>仅允许取消 status=1(已预约)的预约;已取消或非本人预约直接拒绝。
|
||||||
|
* 预约取消后释放名额,DB 部分唯一索引允许同用户同课程同时段重新预约。</p>
|
||||||
|
*
|
||||||
|
* @param appointmentId 预约 ID(路径参数)
|
||||||
|
* @return 空对象
|
||||||
|
*/
|
||||||
|
@PostMapping("/appointment/{appointmentId}/cancel")
|
||||||
|
public Result<Void> cancelAppointment(@PathVariable Long appointmentId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
courseService.cancelAppointment(userId, appointmentId);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Spring Data Page 组装为前端约定的分页数据结构。
|
||||||
|
*/
|
||||||
|
private Map<String, Object> buildPageData(Page<?> page) {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", page.getContent());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.dto.ExamSubmitDTO;
|
||||||
|
import com.haisen.laop.service.ExamService;
|
||||||
|
import com.haisen.laop.vo.ExamBankVO;
|
||||||
|
import com.haisen.laop.vo.ExamQuestionVO;
|
||||||
|
import com.haisen.laop.vo.ExamResultVO;
|
||||||
|
import com.haisen.laop.vo.TrainingStoreVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 培训考试模块 —— 小程序端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>微信小程序(培训考试 Tab → 机构列表 / 题库练习)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/exam/*},与 Web 管理端 {@code /exam/*} 严格隔离。
|
||||||
|
* <p><b>职责:</b>承载面向 C 端用户的培训门店浏览、题库浏览与答题判分(实时、不存储),
|
||||||
|
* 业务逻辑全部委托 {@link ExamService},本层只做入口隔离 + 参数接收。</p>
|
||||||
|
*
|
||||||
|
* <p><b>鉴权策略:</b>所有接口公开访问(浏览培训门店、题库、题目均无需登录)。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/exam")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniExamController {
|
||||||
|
|
||||||
|
private final ExamService examService;
|
||||||
|
|
||||||
|
// ==================== 培训门店(小程序展示) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取培训门店信息(小程序端展示"机构列表"tab 详情)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/training")
|
||||||
|
public Result<TrainingStoreVO> getTrainingStore() {
|
||||||
|
return Result.success(examService.getTrainingStore());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 题库列表 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有题库列表,附带各题库实时聚合的题目数量。
|
||||||
|
*
|
||||||
|
* <p>返回的 {@link ExamBankVO} 包含 questionCount、singleCount、multiCount、judgeCount,
|
||||||
|
* 小程序可直接用于卡片展示。</p>
|
||||||
|
*/
|
||||||
|
@GetMapping("/banks")
|
||||||
|
public Result<List<ExamBankVO>> listBanks() {
|
||||||
|
return Result.success(examService.listBanks());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 小程序答题端 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取指定题库下的题目(不含答案与解析)。
|
||||||
|
*
|
||||||
|
* <p>用于"逐题练习"模式。按 sortOrder 从小到大排序,sortOrder 相同时按 id 升序兜底。</p>
|
||||||
|
*
|
||||||
|
* @param bankId 题库ID
|
||||||
|
* @param pageNo 页码(0 基,首传 0)
|
||||||
|
* @param pageSize 每页条数(默认 20)
|
||||||
|
*/
|
||||||
|
@GetMapping("/banks/{bankId}/questions")
|
||||||
|
public Result<Map<String, Object>> listQuestions(
|
||||||
|
@PathVariable Long bankId,
|
||||||
|
@RequestParam(defaultValue = "0") int pageNo,
|
||||||
|
@RequestParam(defaultValue = "20") int pageSize) {
|
||||||
|
List<ExamQuestionVO> list = examService.listQuestions(bankId, pageNo, pageSize);
|
||||||
|
long total = examService.countQuestions(bankId);
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", list);
|
||||||
|
data.put("total", total);
|
||||||
|
data.put("pageNo", pageNo);
|
||||||
|
data.put("pageSize", pageSize);
|
||||||
|
data.put("totalPages", (int) Math.ceil((double) total / pageSize));
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟考试:从指定题库按配比随机抽题组成试卷。
|
||||||
|
*
|
||||||
|
* <p>配比:30 单选 + 25 多选 + 20 判断 = 75 题 / 100 分。
|
||||||
|
* 每次调用独立随机,题目顺序也会打乱。</p>
|
||||||
|
*
|
||||||
|
* @param bankId 题库ID
|
||||||
|
*/
|
||||||
|
@GetMapping("/banks/{bankId}/mock")
|
||||||
|
public Result<List<ExamQuestionVO>> mockExam(@PathVariable Long bankId) {
|
||||||
|
return Result.success(examService.mockExam(bankId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交答题结果并实时判分(不存储)。
|
||||||
|
*
|
||||||
|
* <p>小程序端"逐题练习"提交全部答案后调用,或"模拟考试"交卷后调用。
|
||||||
|
* 返回 {@link ExamResultVO} 包含每题判分明细 + 汇总分数。</p>
|
||||||
|
*
|
||||||
|
* @param bankId 题库ID
|
||||||
|
* @param dto 用户作答集合
|
||||||
|
*/
|
||||||
|
@PostMapping("/banks/{bankId}/submit")
|
||||||
|
public Result<ExamResultVO> submitExam(
|
||||||
|
@PathVariable Long bankId,
|
||||||
|
@RequestBody ExamSubmitDTO dto) {
|
||||||
|
return Result.success(examService.submitExam(bankId, dto));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.service.HomeService;
|
||||||
|
import com.haisen.laop.vo.HomeMerchantVO;
|
||||||
|
import com.haisen.laop.vo.HomePilotVO;
|
||||||
|
import com.haisen.laop.vo.MerchantDetailVO;
|
||||||
|
import com.haisen.laop.vo.PilotDetailVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序首页展示接口:实力商家 / 本地飞手列表与公开详情。
|
||||||
|
*
|
||||||
|
* <p><b>公开访问:</b>首页内容无需登录即可浏览(与任务大厅一致),
|
||||||
|
* 登录相关动作由具体交互入口(认证、抢单等)自行保障。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/home")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniHomeController {
|
||||||
|
|
||||||
|
private final HomeService homeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页"实力商家"列表:已认证商家,按订单数降序限量返回。
|
||||||
|
*/
|
||||||
|
@GetMapping("/merchants")
|
||||||
|
public Result<List<HomeMerchantVO>> listMerchants() {
|
||||||
|
return Result.success(homeService.listMerchants());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页"本地飞手"列表:已认证飞手,按完成任务数降序限量返回。
|
||||||
|
*/
|
||||||
|
@GetMapping("/pilots")
|
||||||
|
public Result<List<HomePilotVO>> listPilots() {
|
||||||
|
return Result.success(homeService.listPilots());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家列表页:分页查询已认证商家,按订单数降序;支持按商家名称模糊搜索。
|
||||||
|
*/
|
||||||
|
@GetMapping("/merchants/page")
|
||||||
|
public Result<List<HomeMerchantVO>> pageMerchants(@RequestParam("pageNo") int pageNo,
|
||||||
|
@RequestParam("pageSize") int pageSize,
|
||||||
|
@RequestParam(value = "name", required = false) String name) {
|
||||||
|
return Result.success(homeService.pageMerchants(pageNo, pageSize, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手列表页:分页查询已认证飞手,按完成任务数降序;支持按飞手名称模糊搜索。
|
||||||
|
*/
|
||||||
|
@GetMapping("/pilots/page")
|
||||||
|
public Result<List<HomePilotVO>> pagePilots(@RequestParam("pageNo") int pageNo,
|
||||||
|
@RequestParam("pageSize") int pageSize,
|
||||||
|
@RequestParam(value = "name", required = false) String name) {
|
||||||
|
return Result.success(homeService.pagePilots(pageNo, pageSize, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家公开详情:全部商家信息 + 该商家发布的任务列表。
|
||||||
|
*/
|
||||||
|
@GetMapping("/merchant/{id}")
|
||||||
|
public Result<MerchantDetailVO> getMerchantDetail(@PathVariable("id") Long id) {
|
||||||
|
return Result.success(homeService.getMerchantDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手公开详情:飞手画像 + 参与过的任务列表(不含个人手机号)。
|
||||||
|
*/
|
||||||
|
@GetMapping("/pilot/{id}")
|
||||||
|
public Result<PilotDetailVO> getPilotDetail(@PathVariable("id") Long id) {
|
||||||
|
return Result.success(homeService.getPilotDetail(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.common.constants.PointsConstants;
|
||||||
|
import com.haisen.laop.common.enums.PointsEventEnum;
|
||||||
|
import com.haisen.laop.entity.PointsDetail;
|
||||||
|
import com.haisen.laop.entity.User;
|
||||||
|
import com.haisen.laop.repository.PointsDetailRepository;
|
||||||
|
import com.haisen.laop.repository.UserRepository;
|
||||||
|
import com.haisen.laop.service.AppConfigService;
|
||||||
|
import com.haisen.laop.vo.PointsDetailVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分模块 —— 小程序端 Controller,承载"我的-积分明细"列表查询。
|
||||||
|
*
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/points/*}。</p>
|
||||||
|
*
|
||||||
|
* <p><b>分页规约:</b>分页参数由前端显式传入,后端不设默认值与上限兜底。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/points")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniPointsController {
|
||||||
|
|
||||||
|
private final PointsDetailRepository pointsDetailRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final AppConfigService appConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询积分规则参数:发布任务获取积分数量、发布优质任务扣减积分数量。
|
||||||
|
*
|
||||||
|
* <p>供小程序发布任务页渲染"发布优质任务消耗 N 积分"等提示文案,
|
||||||
|
* 避免前端硬编码与 Web 端功能配置不一致;无需登录态。</p>
|
||||||
|
*
|
||||||
|
* @return {publishAwardPoints, premiumDeductPoints}
|
||||||
|
*/
|
||||||
|
@GetMapping("/config")
|
||||||
|
public Result<Map<String, Object>> pointsConfig() {
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("publishAwardPoints", appConfigService.getIntValue(
|
||||||
|
PointsConstants.CONFIG_KEY_POINTS_PUBLISH_AWARD,
|
||||||
|
PointsConstants.DEFAULT_PUBLISH_AWARD_POINTS));
|
||||||
|
data.put("premiumDeductPoints", appConfigService.getIntValue(
|
||||||
|
PointsConstants.CONFIG_KEY_POINTS_PREMIUM_DEDUCT,
|
||||||
|
PointsConstants.DEFAULT_PREMIUM_DEDUCT_POINTS));
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询我的积分明细(增扣流水统一列举,收入为正、支出为负)。
|
||||||
|
*
|
||||||
|
* <p>同时返回"积分汇总"字段供前端顶部展示:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code totalPoints}:当前积分余额(取自 user.points,避免翻页累计偏差);</li>
|
||||||
|
* <li>{@code consumedPoints}:累计已消耗积分(仅统计支出流水 points 绝对值之和)。</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码,从 1 开始,前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @return {list, totalElements, totalPages, pageNo, pageSize, totalPoints, consumedPoints}
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public Result<Map<String, Object>> myPointsDetail(
|
||||||
|
@RequestParam Integer pageNo,
|
||||||
|
@RequestParam Integer pageSize) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new BusinessException("用户不存在"));
|
||||||
|
Long consumed = pointsDetailRepository.sumConsumedPointsByUserId(userId);
|
||||||
|
if (consumed == null) {
|
||||||
|
consumed = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(pageNo - 1, pageSize);
|
||||||
|
|
||||||
|
Page<PointsDetail> page = pointsDetailRepository
|
||||||
|
.findByUserIdAndDeletedAtIsNullOrderByCreatedAtDesc(userId, pageable);
|
||||||
|
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("list", page.getContent().stream().map(this::toVO).toList());
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", pageNo);
|
||||||
|
data.put("pageSize", pageSize);
|
||||||
|
// 顶部汇总:余额取当前 user.points(权威值),消耗取流水 SUM 兜底 0
|
||||||
|
data.put("totalPoints", user.getPoints() == null ? 0 : user.getPoints());
|
||||||
|
data.put("consumedPoints", consumed);
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流水 PO 转 VO:事件名称按编码翻译,前端直接展示。
|
||||||
|
*/
|
||||||
|
private PointsDetailVO toVO(PointsDetail detail) {
|
||||||
|
PointsEventEnum event = PointsEventEnum.of(detail.getEventType());
|
||||||
|
return PointsDetailVO.builder()
|
||||||
|
.changeType(detail.getChangeType())
|
||||||
|
.eventType(detail.getEventType())
|
||||||
|
.eventName(event == null ? "积分变动" : event.getDesc())
|
||||||
|
.points(detail.getPoints())
|
||||||
|
.remark(detail.getRemark())
|
||||||
|
.createdAt(detail.getCreatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.service.PromoteService;
|
||||||
|
import com.haisen.laop.vo.PromoteQrVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推广模块 —— 小程序端 Controller,承载"飞手推广"二维码生成。
|
||||||
|
*
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/promote/*}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/promote")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniPromoteController {
|
||||||
|
|
||||||
|
private final PromoteService promoteService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成我的专属推广小程序码(scene 携带我的 userId)。
|
||||||
|
*
|
||||||
|
* <p>好友扫码进入小程序并注册成功后,我方账户 +4 积分(登录链路自动完成)。</p>
|
||||||
|
*
|
||||||
|
* @return 小程序码 Base64 字符串
|
||||||
|
*/
|
||||||
|
@GetMapping("/qrcode")
|
||||||
|
public Result<PromoteQrVO> getQrCode() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
String qrCodeBase64 = promoteService.getPromoteQrCode(userId);
|
||||||
|
return Result.success(PromoteQrVO.builder().qrCodeBase64(qrCodeBase64).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,697 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.common.enums.CertStatusEnum;
|
||||||
|
import com.haisen.laop.common.enums.InsuranceTypeEnum;
|
||||||
|
import com.haisen.laop.common.enums.TaskRecordStatusEnum;
|
||||||
|
import com.haisen.laop.common.enums.TaskStatusEnum;
|
||||||
|
import com.haisen.laop.common.enums.TaskTypeEnum;
|
||||||
|
import com.haisen.laop.dto.TaskHallQueryDTO;
|
||||||
|
import com.haisen.laop.dto.TaskPublishDTO;
|
||||||
|
import com.haisen.laop.dto.TaskReviewDTO;
|
||||||
|
import com.haisen.laop.entity.SysFile;
|
||||||
|
import com.haisen.laop.entity.Task;
|
||||||
|
import com.haisen.laop.entity.TaskRecord;
|
||||||
|
import com.haisen.laop.entity.Merchant;
|
||||||
|
import com.haisen.laop.entity.Pilot;
|
||||||
|
import com.haisen.laop.repository.MerchantRepository;
|
||||||
|
import com.haisen.laop.repository.PilotRepository;
|
||||||
|
import com.haisen.laop.repository.SysFileRepository;
|
||||||
|
import com.haisen.laop.repository.TaskRecordRepository;
|
||||||
|
import com.haisen.laop.repository.TaskRepository;
|
||||||
|
import com.haisen.laop.service.TaskService;
|
||||||
|
import com.haisen.laop.vo.TaskApplicantVO;
|
||||||
|
import com.haisen.laop.vo.TaskPublishVO;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务模块 —— 小程序端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>消费端:</b>微信小程序(飞手/商家在手机上操作)。
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/task/*},与 Web 后台端 /task/* 严格隔离。
|
||||||
|
* <p><b>职责:</b>承载面向 C 端用户的任务操作(发布、抢单、任务列表、详情、取消、完成等),
|
||||||
|
* 不包含后台管理操作(审核、置顶等由 Web 后台的
|
||||||
|
* {@link com.haisen.laop.controller.TaskController} 承载)。</p>
|
||||||
|
*
|
||||||
|
* <p>与 Web 端共享同一个 {@link TaskService},Controller 层仅做入口隔离 + 参数接收 + 鉴权注入。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/task")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniTaskController {
|
||||||
|
|
||||||
|
private final TaskService taskService;
|
||||||
|
private final TaskRepository taskRepository;
|
||||||
|
private final TaskRecordRepository taskRecordRepository;
|
||||||
|
private final SysFileRepository sysFileRepository;
|
||||||
|
private final MerchantRepository merchantRepository;
|
||||||
|
private final PilotRepository pilotRepository;
|
||||||
|
|
||||||
|
/** 文件访问基础 URL(开发环境 http://localhost:8081/laop,生产填实际域名) */
|
||||||
|
@Value("${laop.file-base-url:}")
|
||||||
|
private String fileBaseUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家发布新任务。
|
||||||
|
*
|
||||||
|
* <p>请求头需携带 {@code Authorization: Bearer {userId}},
|
||||||
|
* 发布人身份由 {@link RequestContext} 自动注入。</p>
|
||||||
|
*
|
||||||
|
* @param publishDTO 发布表单全部字段(见原型图"发布任务"页面)
|
||||||
|
* @return 发布成功后的 taskId + taskNo
|
||||||
|
*/
|
||||||
|
@PostMapping("/publish")
|
||||||
|
public Result<TaskPublishVO> publish(@Valid @RequestBody TaskPublishDTO publishDTO) {
|
||||||
|
Long publisherId = RequestContext.getUserId();
|
||||||
|
if (publisherId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
// 商家认证校验:只有已认证商家才能发布任务,非已认证状态直接拒绝
|
||||||
|
Merchant merchant = merchantRepository.findByUserIdAndDeletedAtIsNull(publisherId);
|
||||||
|
if (merchant == null || !Objects.equals(merchant.getCertStatus(), CertStatusEnum.CERTIFIED.getCode())) {
|
||||||
|
Integer status = merchant != null
|
||||||
|
? merchant.getCertStatus()
|
||||||
|
: CertStatusEnum.NONE.getCode();
|
||||||
|
CertStatusEnum certStatus = CertStatusEnum.of(status);
|
||||||
|
String reason = certStatus == null ? "商家认证异常,请重新认证" : switch (certStatus) {
|
||||||
|
case NONE -> "请先完成商家认证后再发布任务";
|
||||||
|
case PENDING -> "商家认证审核中,通过后即可发布任务";
|
||||||
|
case REJECTED -> "商家认证未通过,请重新提交认证";
|
||||||
|
case REVOKED -> "您的商家资格已被取消";
|
||||||
|
// 外层已排除已认证态,此分支理论不可达,仅为 switch 穷举
|
||||||
|
case CERTIFIED -> "商家认证异常,请重新认证";
|
||||||
|
};
|
||||||
|
throw new BusinessException(403, reason);
|
||||||
|
}
|
||||||
|
TaskPublishVO vo = taskService.publish(publisherId, publishDTO);
|
||||||
|
return Result.success(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的发布任务列表(商家视角),按 tab 筛选状态,按创建时间倒序,分页。
|
||||||
|
*
|
||||||
|
* <p>tab 取值:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code published}(默认)— 已发布:状态 IN 待接单+已接单</li>
|
||||||
|
* <li>{@code running} — 进行中:状态 = 执行中</li>
|
||||||
|
* <li>{@code done} — 已完成:状态 = 已完成</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基),前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @param tab 状态分组(published/running/done),默认 published
|
||||||
|
* @return {list, totalElements, totalPages, pageNo, pageSize}
|
||||||
|
*/
|
||||||
|
@GetMapping("/my")
|
||||||
|
public Result<Map<String, Object>> myTasks(
|
||||||
|
@RequestParam int pageNo,
|
||||||
|
@RequestParam int pageSize,
|
||||||
|
@RequestParam(defaultValue = "published") String tab) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize);
|
||||||
|
Page<Task> page;
|
||||||
|
switch (tab) {
|
||||||
|
case "running" -> page = taskRepository
|
||||||
|
.findByPublisherIdAndStatusAndDeletedAtIsNullOrderByCreatedAtDesc(
|
||||||
|
userId, TaskStatusEnum.EXECUTING.getCode(), pageable);
|
||||||
|
case "done" -> page = taskRepository
|
||||||
|
.findByPublisherIdAndStatusAndDeletedAtIsNullOrderByCreatedAtDesc(
|
||||||
|
userId, TaskStatusEnum.COMPLETED.getCode(), pageable);
|
||||||
|
default -> page = taskRepository
|
||||||
|
.findByPublisherIdAndStatusInAndDeletedAtIsNullOrderByCreatedAtDesc(
|
||||||
|
userId, List.of(TaskStatusEnum.PENDING.getCode(), TaskStatusEnum.ACCEPTED.getCode()),
|
||||||
|
pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量统计各任务的待确认申请数 + 首位申请人姓名(仅"已发布"tab 需要)
|
||||||
|
Set<Long> taskIds = page.getContent().stream().map(Task::getId).collect(Collectors.toSet());
|
||||||
|
Map<Long, Long> applicantCounts = null;
|
||||||
|
Map<Long, String> firstApplicantNames = null;
|
||||||
|
if ("published".equals(tab)) {
|
||||||
|
applicantCounts = taskService.countApplicants(taskIds);
|
||||||
|
firstApplicantNames = collectFirstApplicantNameMap(taskIds);
|
||||||
|
}
|
||||||
|
return Result.success(buildPageData(page, applicantCounts, firstApplicantNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家"我的任务"各 tab 计数(已发布/进行中/已完成),用于个人中心徽标。
|
||||||
|
*
|
||||||
|
* @return { published, running, done }
|
||||||
|
*/
|
||||||
|
@GetMapping("/my/counts")
|
||||||
|
public Result<Map<String, Long>> myTaskCounts() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(taskService.countMyTasksByTabs(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的接单任务列表(飞手视角),按 tab 筛选状态,以申请记录为主线分页,最新申请在前。
|
||||||
|
*
|
||||||
|
* <p>tab 取值:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code accepted}(默认)— 已接单:申请活跃(待确认+已同意)且任务未开始(待接单+已接单)</li>
|
||||||
|
* <li>{@code running} — 进行中:飞手已同意且任务执行中</li>
|
||||||
|
* <li>{@code done} — 已完成:飞手已同意且任务已完成</li>
|
||||||
|
* </ul>
|
||||||
|
* 仅展示活跃申请,已拒绝/已撤销的记录不在任何 tab 展示。</p>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基),前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @param tab 状态分组(accepted/running/done),默认 accepted
|
||||||
|
* @return {list, totalElements, totalPages, pageNo, pageSize}
|
||||||
|
*/
|
||||||
|
@GetMapping("/accepted")
|
||||||
|
public Result<Map<String, Object>> myAcceptedTasks(
|
||||||
|
@RequestParam int pageNo,
|
||||||
|
@RequestParam int pageSize,
|
||||||
|
@RequestParam(defaultValue = "accepted") String tab) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 按 tab 确定申请记录状态集合 + 任务状态集合
|
||||||
|
List<Integer> recordStatuses;
|
||||||
|
List<Integer> taskStatuses;
|
||||||
|
switch (tab) {
|
||||||
|
case "running" -> {
|
||||||
|
recordStatuses = List.of(TaskRecordStatusEnum.APPROVED.getCode());
|
||||||
|
taskStatuses = List.of(TaskStatusEnum.EXECUTING.getCode());
|
||||||
|
}
|
||||||
|
case "done" -> {
|
||||||
|
recordStatuses = List.of(TaskRecordStatusEnum.APPROVED.getCode());
|
||||||
|
taskStatuses = List.of(TaskStatusEnum.COMPLETED.getCode());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
recordStatuses = List.of(TaskRecordStatusEnum.APPLIED.getCode(),
|
||||||
|
TaskRecordStatusEnum.APPROVED.getCode());
|
||||||
|
taskStatuses = List.of(TaskStatusEnum.PENDING.getCode(), TaskStatusEnum.ACCEPTED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 以申请记录为主线分页(按记录状态+任务状态子查询过滤)
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize);
|
||||||
|
Page<TaskRecord> recordPage = taskRecordRepository
|
||||||
|
.findByUserIdAndRecordStatusesAndTaskStatuses(userId, recordStatuses, taskStatuses, pageable);
|
||||||
|
List<TaskRecord> records = recordPage.getContent();
|
||||||
|
|
||||||
|
// 2. 分步批量查任务(先取记录页 → IN 批量取任务),一对多关联禁止 JOIN 膨胀
|
||||||
|
Set<Long> taskIds = records.stream().map(TaskRecord::getTaskId).collect(Collectors.toSet());
|
||||||
|
Map<Long, Task> taskMap = taskIds.isEmpty()
|
||||||
|
? Map.of()
|
||||||
|
: taskRepository.findByIdInAndDeletedAtIsNull(taskIds).stream()
|
||||||
|
.collect(Collectors.toMap(Task::getId, t -> t, (a, b) -> a));
|
||||||
|
|
||||||
|
// 3. 组装 VO:任务字段与 /my、/hall 完全一致 + 申请进度字段(场景图 URL 批量解析,避免 N+1)
|
||||||
|
List<Task> pageTasks = records.stream()
|
||||||
|
.map(r -> taskMap.get(r.getTaskId()))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<Long, String> fileUrlMap = collectFileUrlMap(pageTasks);
|
||||||
|
Map<Long, String> publisherNameMap = collectPublisherNameMap(pageTasks);
|
||||||
|
Map<Long, String> acceptedPilotNameMap = collectAcceptedPilotNameMap(pageTasks);
|
||||||
|
List<Map<String, Object>> voList = new ArrayList<>();
|
||||||
|
for (TaskRecord record : records) {
|
||||||
|
Task task = taskMap.get(record.getTaskId());
|
||||||
|
if (task == null) {
|
||||||
|
// 任务已被软删的脏记录跳过展示,不影响分页统计
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, Object> vo = buildTaskVO(task, fileUrlMap, publisherNameMap, acceptedPilotNameMap);
|
||||||
|
vo.put("recordStatus", record.getStatus());
|
||||||
|
TaskRecordStatusEnum recordStatus = TaskRecordStatusEnum.of(record.getStatus());
|
||||||
|
vo.put("recordStatusName", recordStatus != null ? recordStatus.getDesc() : "未知");
|
||||||
|
voList.add(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", voList);
|
||||||
|
data.put("totalElements", recordPage.getTotalElements());
|
||||||
|
data.put("totalPages", recordPage.getTotalPages());
|
||||||
|
data.put("pageNo", recordPage.getNumber());
|
||||||
|
data.put("pageSize", recordPage.getSize());
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抢单大厅(飞手视角):全平台可抢(待接单)任务,按创建时间倒序,分页。
|
||||||
|
*
|
||||||
|
* <p>支持三个可选筛选(组合使用):
|
||||||
|
* <ul>
|
||||||
|
* <li>省市区 — {@code province}/{@code city}/{@code district},按最精确层级命中</li>
|
||||||
|
* <li>任务类型 — {@code type} 编码(1植保 2吊运 3航拍 4巡检 5清洗 6表演 7测绘)</li>
|
||||||
|
* <li>金额区间 — {@code minReward}/{@code maxReward}(元,闭区间,可单边)</li>
|
||||||
|
* </ul>
|
||||||
|
* 筛选拼装逻辑在 {@link TaskService#hall},VO 组装与 /my 完全一致。</p>
|
||||||
|
*
|
||||||
|
* @param pageNo 页码(0 基),前端必传
|
||||||
|
* @param pageSize 每页条数,前端必传
|
||||||
|
* @param query 筛选条件(GET 参数按字段名自动绑定,全部可选)
|
||||||
|
* @return {list, totalElements, totalPages, pageNo, pageSize}
|
||||||
|
*/
|
||||||
|
@GetMapping("/hall")
|
||||||
|
public Result<Map<String, Object>> taskHall(
|
||||||
|
@RequestParam int pageNo,
|
||||||
|
@RequestParam int pageSize,
|
||||||
|
TaskHallQueryDTO query) {
|
||||||
|
Pageable pageable = PageRequest.of(pageNo, pageSize);
|
||||||
|
Page<Task> page = taskService.hall(query, pageable);
|
||||||
|
return Result.success(buildPageData(page, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手申请接单(抢单)。
|
||||||
|
*
|
||||||
|
* <p>业务校验(飞手认证、任务状态、重复申请等)在 {@link TaskService#grab};
|
||||||
|
* 申请成功后事务提交自动触发微信模板消息通知商家(队列异步发送,不阻塞本请求)。</p>
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
* @return 申请成功提示
|
||||||
|
*/
|
||||||
|
@PostMapping("/{taskId}/grab")
|
||||||
|
public Result<Void> grab(@PathVariable Long taskId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
taskService.grab(taskId, userId);
|
||||||
|
return Result.success(null, "申请已提交,等待商家确认");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手撤销接单申请。
|
||||||
|
*
|
||||||
|
* <p>申请后可随时撤销;已被商家同意(已接单)的申请撤销后任务回滚为待接单,
|
||||||
|
* 任务开始后不可撤销。撤销成功后事务提交自动触发微信模板消息通知商家。</p>
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID(按 (task_id, user_id) 定位申请记录,前端无需感知 recordId)
|
||||||
|
* @return 撤销结果提示
|
||||||
|
*/
|
||||||
|
@PostMapping("/{taskId}/withdraw")
|
||||||
|
public Result<Void> withdraw(@PathVariable Long taskId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
taskService.withdraw(taskId, userId);
|
||||||
|
return Result.success(null, "已撤销申请");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家查看任务的申请飞手列表。
|
||||||
|
*
|
||||||
|
* <p>仅任务发布人可查;返回申请人概要(姓名/电话/认证状态/申请时间/状态),
|
||||||
|
* 执照资质详情由前端跳转飞手详情页查看,支持商家在此列表直接同意/拒绝。</p>
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
* @return 申请飞手列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/{taskId}/applicants")
|
||||||
|
public Result<List<TaskApplicantVO>> applicants(@PathVariable Long taskId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(taskService.applicants(taskId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家审批申请:同意(任务进入已接单)或拒绝。
|
||||||
|
*
|
||||||
|
* @param recordId 申请记录 ID
|
||||||
|
* @param dto 审批结果与拒绝原因
|
||||||
|
* @return 审批结果提示
|
||||||
|
*/
|
||||||
|
@PostMapping("/records/{recordId}/review")
|
||||||
|
public Result<Void> review(@PathVariable Long recordId, @Valid @RequestBody TaskReviewDTO dto) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
taskService.review(recordId, userId, dto);
|
||||||
|
return Result.success(null, Boolean.TRUE.equals(dto.getApprove()) ? "已同意接单" : "已拒绝该申请");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 商家任务状态流转:撤回 / 确认开始 / 确认完成 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家撤回任务:将已发布(待接单/已接单)的任务置为已取消。
|
||||||
|
*
|
||||||
|
* <p>若已有飞手接单,撤回后飞手侧申请记录同步关闭并收到通知。
|
||||||
|
* 撤回不可逆,任务不再出现在商家"已发布/进行中"列表中。</p>
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
* @return 撤回结果提示
|
||||||
|
*/
|
||||||
|
@PostMapping("/{taskId}/recall")
|
||||||
|
public Result<Void> recall(@PathVariable Long taskId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
taskService.recall(taskId, userId);
|
||||||
|
return Result.success(null, "任务已撤回");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家确认开始:将已发布(待接单/已接单)的任务推进为执行中。
|
||||||
|
*
|
||||||
|
* <p>无需飞手已接单也可直接开始;开始后待确认申请自动关闭。</p>
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
* @return 开始结果提示
|
||||||
|
*/
|
||||||
|
@PostMapping("/{taskId}/start")
|
||||||
|
public Result<Void> start(@PathVariable Long taskId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
taskService.start(taskId, userId);
|
||||||
|
return Result.success(null, "任务已开始");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家确认完成:将执行中的任务推进为已完成。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
* @return 完成结果提示
|
||||||
|
*/
|
||||||
|
@PostMapping("/{taskId}/complete")
|
||||||
|
public Result<Void> complete(@PathVariable Long taskId) {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
taskService.complete(taskId, userId);
|
||||||
|
return Result.success(null, "任务已完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 飞手任务计数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手"我的任务"各 tab 计数(已接单/进行中/已完成),用于个人中心徽标。
|
||||||
|
*
|
||||||
|
* @return { accepted, running, done }
|
||||||
|
*/
|
||||||
|
@GetMapping("/accepted/counts")
|
||||||
|
public Result<Map<String, Long>> myAcceptedCounts() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(taskService.countMyAcceptedByTabs(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将分页任务实体组装为前端列表数据(场景图 URL / 区域文本 / 保险名称 / 类型名)。
|
||||||
|
*
|
||||||
|
* <p>/my 与 /hall 共用,保证两端卡片渲染字段完全一致。</p>
|
||||||
|
*
|
||||||
|
* @param applicantCounts taskId → 待确认申请数(仅 /my 传入,其余端传 null 不输出该字段)
|
||||||
|
* @param firstApplicantNames taskId → 首位待确认申请人姓名(仅商家"已发布"tab 传入)
|
||||||
|
*/
|
||||||
|
private Map<String, Object> buildPageData(Page<Task> page, Map<Long, Long> applicantCounts,
|
||||||
|
Map<Long, String> firstApplicantNames) {
|
||||||
|
List<Task> tasks = page.getContent();
|
||||||
|
Map<Long, String> fileUrlMap = collectFileUrlMap(tasks);
|
||||||
|
Map<Long, String> publisherNameMap = collectPublisherNameMap(tasks);
|
||||||
|
Map<Long, String> acceptedPilotNameMap = collectAcceptedPilotNameMap(tasks);
|
||||||
|
|
||||||
|
// —— 逐条组装 VO ——
|
||||||
|
List<Map<String, Object>> voList = new ArrayList<>();
|
||||||
|
for (Task task : tasks) {
|
||||||
|
Map<String, Object> vo = buildTaskVO(task, fileUrlMap, publisherNameMap, acceptedPilotNameMap);
|
||||||
|
// 待确认申请数 + 首位申请人姓名(仅商家"我的发布"场景传入;其余接口不输出)
|
||||||
|
if (applicantCounts != null) {
|
||||||
|
vo.put("applicantCount", applicantCounts.getOrDefault(task.getId(), 0L));
|
||||||
|
}
|
||||||
|
if (firstApplicantNames != null) {
|
||||||
|
vo.put("firstApplicantName", firstApplicantNames.get(task.getId()));
|
||||||
|
}
|
||||||
|
voList.add(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("list", voList);
|
||||||
|
data.put("totalElements", page.getTotalElements());
|
||||||
|
data.put("totalPages", page.getTotalPages());
|
||||||
|
data.put("pageNo", page.getNumber());
|
||||||
|
data.put("pageSize", page.getSize());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量收集任务的场景图 fileId → 批量查 SysFile → 构建绝对 URL 映射。
|
||||||
|
*
|
||||||
|
* <p>分步批量查(先收集 fileId 再 IN 查询),避免逐条任务查库造成 N+1。</p>
|
||||||
|
*/
|
||||||
|
private Map<Long, String> collectFileUrlMap(List<Task> tasks) {
|
||||||
|
Set<Long> allFileIds = new HashSet<>();
|
||||||
|
for (Task task : tasks) {
|
||||||
|
if (StringUtils.isNotBlank(task.getSceneImages())) {
|
||||||
|
for (String idStr : task.getSceneImages().split(";")) {
|
||||||
|
try {
|
||||||
|
allFileIds.add(Long.parseLong(idStr.trim()));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 防御性忽略非法 ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allFileIds.isEmpty()
|
||||||
|
? Map.of()
|
||||||
|
: sysFileRepository.findAllByIdIn(allFileIds).stream()
|
||||||
|
.collect(Collectors.toMap(SysFile::getId, SysFile::getFileUrl, (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量收集任务的发布商家名称,构建 publisherId → 商家名称 映射。
|
||||||
|
*
|
||||||
|
* <p>分步批量查(先收集 publisherId 再 IN 查询 merchant.user_id),避免逐条查库 N+1。
|
||||||
|
* 非商家用户发布的任务(如管理员直发)在映射中缺失,前端展示时降级为"匿名商家"。</p>
|
||||||
|
*/
|
||||||
|
private Map<Long, String> collectPublisherNameMap(List<Task> tasks) {
|
||||||
|
Set<Long> publisherIds = new HashSet<>();
|
||||||
|
for (Task task : tasks) {
|
||||||
|
if (task.getPublisherId() != null) {
|
||||||
|
publisherIds.add(task.getPublisherId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (publisherIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return merchantRepository.findByUserIdInAndDeletedAtIsNull(publisherIds).stream()
|
||||||
|
.collect(Collectors.toMap(Merchant::getUserId, Merchant::getName, (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量收集任务的已接单飞手姓名,构建 acceptedPilotId → 飞手名称 映射。
|
||||||
|
*
|
||||||
|
* <p>分步批量查(先收集 acceptedPilotId 再 IN 查询),避免逐条查库 N+1。
|
||||||
|
* 无已接单飞手的任务 acceptedPilotId 为 null,自动跳过。</p>
|
||||||
|
*/
|
||||||
|
private Map<Long, String> collectAcceptedPilotNameMap(List<Task> tasks) {
|
||||||
|
Set<Long> pilotIds = new HashSet<>();
|
||||||
|
for (Task task : tasks) {
|
||||||
|
if (task.getAcceptedPilotId() != null) {
|
||||||
|
pilotIds.add(task.getAcceptedPilotId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pilotIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return pilotRepository.findByIdInAndDeletedAtIsNull(pilotIds).stream()
|
||||||
|
.collect(Collectors.toMap(Pilot::getId, Pilot::getName, (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询任务的首位待确认申请人姓名,构建 taskId → 申请人姓名 映射。
|
||||||
|
*
|
||||||
|
* <p>仅返回每个任务最早申请(待确认态)的飞手姓名,用于商家"我的发布"列表卡片,
|
||||||
|
* 配合"1人申请"徽标展示具体申请人,避免商家点击进入后才知道是谁。</p>
|
||||||
|
*/
|
||||||
|
private Map<Long, String> collectFirstApplicantNameMap(Set<Long> taskIds) {
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<Object[]> rows = taskRecordRepository.findFirstApplicantByTaskIds(
|
||||||
|
taskIds, TaskRecordStatusEnum.APPLIED.getCode());
|
||||||
|
return rows.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> (Long) row[0],
|
||||||
|
row -> (String) row[2],
|
||||||
|
(a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个任务实体组装为前端列表卡片 VO(场景图 URL / 区域文本 / 保险名称 / 类型名 / 发布商家名称 / 已接单飞手姓名)。
|
||||||
|
*
|
||||||
|
* <p>/my、/hall、/accepted 三端共用,保证列表卡片渲染字段完全一致。</p>
|
||||||
|
*
|
||||||
|
* @param fileUrlMap fileId → 绝对 URL
|
||||||
|
* @param publisherNameMap publisherId → 商家名称
|
||||||
|
* @param acceptedPilotNameMap acceptedPilotId → 飞手名称
|
||||||
|
*/
|
||||||
|
private Map<String, Object> buildTaskVO(Task task, Map<Long, String> fileUrlMap,
|
||||||
|
Map<Long, String> publisherNameMap,
|
||||||
|
Map<Long, String> acceptedPilotNameMap) {
|
||||||
|
Map<String, Object> vo = new HashMap<>();
|
||||||
|
// Task 实体核心字段(手动复制,避免直接序列化暴露不必要的字段)
|
||||||
|
vo.put("id", task.getId());
|
||||||
|
vo.put("taskNo", task.getTaskNo());
|
||||||
|
vo.put("name", task.getName());
|
||||||
|
vo.put("type", task.getType());
|
||||||
|
vo.put("province", task.getProvince());
|
||||||
|
vo.put("city", task.getCity());
|
||||||
|
vo.put("district", task.getDistrict());
|
||||||
|
vo.put("address", task.getAddress());
|
||||||
|
vo.put("latitude", task.getLatitude());
|
||||||
|
vo.put("longitude", task.getLongitude());
|
||||||
|
vo.put("sceneImages", task.getSceneImages());
|
||||||
|
vo.put("requiredCount", task.getRequiredCount());
|
||||||
|
vo.put("taskStartTime", task.getTaskStartTime());
|
||||||
|
vo.put("taskEndTime", task.getTaskEndTime());
|
||||||
|
vo.put("reward", task.getReward());
|
||||||
|
vo.put("insuranceRequire", task.getInsuranceRequire());
|
||||||
|
vo.put("contactName", task.getContactName());
|
||||||
|
vo.put("contactPhone", task.getContactPhone());
|
||||||
|
vo.put("remark", task.getRemark());
|
||||||
|
vo.put("status", task.getStatus());
|
||||||
|
// 任务状态中文名(已取消等),用于"我的发布/我的接单"列表展示状态标签
|
||||||
|
TaskStatusEnum statusEnum = TaskStatusEnum.of(task.getStatus());
|
||||||
|
vo.put("statusName", statusEnum != null ? statusEnum.getDesc() : "未知");
|
||||||
|
vo.put("isPremium", task.getIsPremium());
|
||||||
|
vo.put("publisherId", task.getPublisherId());
|
||||||
|
// 发布商家名称(批量预查映射,缺失时前端降级展示"匿名商家")
|
||||||
|
String publisherName = task.getPublisherId() != null ? publisherNameMap.get(task.getPublisherId()) : null;
|
||||||
|
vo.put("publisherName", publisherName);
|
||||||
|
vo.put("acceptedPilotId", task.getAcceptedPilotId());
|
||||||
|
// 已接单飞手名称(acceptedPilotId 有值时解析;未接单/飞手已注销则为 null)
|
||||||
|
String acceptedPilotName = task.getAcceptedPilotId() != null
|
||||||
|
? acceptedPilotNameMap.get(task.getAcceptedPilotId()) : null;
|
||||||
|
vo.put("acceptedPilotName", acceptedPilotName);
|
||||||
|
vo.put("createdAt", task.getCreatedAt());
|
||||||
|
vo.put("updatedAt", task.getUpdatedAt());
|
||||||
|
|
||||||
|
// —— 扩展组装字段 ——
|
||||||
|
// 1. 场景图 URL 列表(绝对 URL,前端可直接用)
|
||||||
|
List<String> sceneImageUrls = new ArrayList<>();
|
||||||
|
if (StringUtils.isNotBlank(task.getSceneImages())) {
|
||||||
|
for (String idStr : task.getSceneImages().split(";")) {
|
||||||
|
try {
|
||||||
|
Long fileId = Long.parseLong(idStr.trim());
|
||||||
|
String url = fileUrlMap.get(fileId);
|
||||||
|
if (StringUtils.isNotBlank(url)) {
|
||||||
|
sceneImageUrls.add(buildFileUrl(url));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 防御性忽略非法 ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vo.put("sceneImageUrls", sceneImageUrls);
|
||||||
|
|
||||||
|
// 2. 区域合并文本(省·市),直辖市 province==city 时只展示城市
|
||||||
|
String province = StringUtils.trimToNull(task.getProvince());
|
||||||
|
String city = StringUtils.trimToNull(task.getCity());
|
||||||
|
String regionText;
|
||||||
|
if (province != null && province.equals(city)) {
|
||||||
|
regionText = city;
|
||||||
|
} else if (province != null && city != null) {
|
||||||
|
regionText = province + "·" + city;
|
||||||
|
} else {
|
||||||
|
regionText = StringUtils.defaultIfBlank(province, city);
|
||||||
|
}
|
||||||
|
vo.put("regionText", regionText);
|
||||||
|
|
||||||
|
// 3. 保险类型名称列表
|
||||||
|
List<String> insuranceNames = new ArrayList<>();
|
||||||
|
if (StringUtils.isNotBlank(task.getInsuranceRequire())) {
|
||||||
|
for (String codeStr : task.getInsuranceRequire().split(";")) {
|
||||||
|
try {
|
||||||
|
Integer code = Integer.parseInt(codeStr.trim());
|
||||||
|
InsuranceTypeEnum type = InsuranceTypeEnum.of(code);
|
||||||
|
if (type != null) {
|
||||||
|
insuranceNames.add(type.getDesc());
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 防御性忽略非法编码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vo.put("insuranceNames", insuranceNames);
|
||||||
|
|
||||||
|
// 4. 任务类型中文名称
|
||||||
|
TaskTypeEnum typeEnum = TaskTypeEnum.of(task.getType());
|
||||||
|
vo.put("typeName", typeEnum != null ? typeEnum.getDesc() : "其他");
|
||||||
|
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将相对文件路径拼接为绝对 URL。
|
||||||
|
*
|
||||||
|
* <p>如果 fileBaseUrl 已配置且 url 是相对路径(以 '/' 开头),则拼接前缀;
|
||||||
|
* 如果 url 已经是 http(s) 绝对路径,直接返回。</p>
|
||||||
|
*
|
||||||
|
* @param url SysFile.fileUrl(可能是相对路径 /static/xxx 或绝对路径)
|
||||||
|
* @return 可直接被小程序 Image src 使用的完整 URL
|
||||||
|
*/
|
||||||
|
private String buildFileUrl(String url) {
|
||||||
|
if (StringUtils.isBlank(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
// 已经是绝对 URL → 直接返回
|
||||||
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
// 没有配置前缀 → 原样返回
|
||||||
|
if (StringUtils.isBlank(fileBaseUrl)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
// 确保前缀不以 '/' 结尾,url 以 '/' 开头
|
||||||
|
String prefix = fileBaseUrl.endsWith("/")
|
||||||
|
? fileBaseUrl.substring(0, fileBaseUrl.length() - 1)
|
||||||
|
: fileBaseUrl;
|
||||||
|
String path = url.startsWith("/") ? url : "/" + url;
|
||||||
|
return prefix + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 小程序端任务操作待扩展:
|
||||||
|
// GET /api/mini/task/{id} 任务详情
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.haisen.laop.controller.mini;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.BusinessException;
|
||||||
|
import com.haisen.laop.common.RequestContext;
|
||||||
|
import com.haisen.laop.common.Result;
|
||||||
|
import com.haisen.laop.service.WxMpSubscribeService;
|
||||||
|
import com.haisen.laop.vo.WxSubscribeStatusVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信能力 —— 小程序端 Controller。
|
||||||
|
*
|
||||||
|
* <p><b>路径前缀:</b>{@code /api/mini/wx/*},承载小程序端与微信生态相关的辅助查询
|
||||||
|
* (服务号关注状态等),与回调入口 /api/wechat/mp/*(微信服务器调用)严格分离。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mini/wx")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MiniWxController {
|
||||||
|
|
||||||
|
private final WxMpSubscribeService wxMpSubscribeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户是否关注服务号。
|
||||||
|
*
|
||||||
|
* <p><b>使用场景:</b>商家发布任务成功后、飞手申请接单成功后,前端调用本接口;
|
||||||
|
* 未关注时弹"关注公众号"引导弹窗(弹窗不阻断已成功的主流程),
|
||||||
|
* 已关注时静默通过。产品约定:提示文案区分角色 —— 商家提示"飞手接单即刻获得通知",
|
||||||
|
* 飞手提示"商家审核后即刻获得通知"。</p>
|
||||||
|
*
|
||||||
|
* @return {subscribed: true 已关注 / false 未关注}
|
||||||
|
*/
|
||||||
|
@GetMapping("/mp/subscribe-status")
|
||||||
|
public Result<WxSubscribeStatusVO> subscribeStatus() {
|
||||||
|
Long userId = RequestContext.getUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException(401, "请先登录");
|
||||||
|
}
|
||||||
|
return Result.success(WxSubscribeStatusVO.builder()
|
||||||
|
.subscribed(wxMpSubscribeService.isSubscribed(userId))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/java/com/haisen/laop/dto/AnnouncementQueryDTO.java
Normal file
40
src/main/java/com/haisen/laop/dto/AnnouncementQueryDTO.java
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.constants.AnnouncementConstants;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告列表查询 DTO,对应 Web 管理端公告列表的筛选条件(GET 参数自动绑定)。
|
||||||
|
*
|
||||||
|
* <p><b>全部字段可选:</b>不传即查全量;{@code pageNo} 从 0 开始(与小程序端分页约定一致),
|
||||||
|
* 前端页码由 api 层负责换算。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AnnouncementQueryDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告标题,模糊匹配(LIKE %title%)。
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告状态:0隐藏 1显示,精确匹配;不传查全部。
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码,从 0 开始,默认 0。
|
||||||
|
*/
|
||||||
|
private Integer pageNo = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页条数,默认 10,上限 {@link AnnouncementConstants#MAX_PAGE_SIZE}。
|
||||||
|
*/
|
||||||
|
private Integer pageSize = AnnouncementConstants.DEFAULT_PAGE_SIZE;
|
||||||
|
}
|
||||||
42
src/main/java/com/haisen/laop/dto/AnnouncementSaveDTO.java
Normal file
42
src/main/java/com/haisen/laop/dto/AnnouncementSaveDTO.java
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告新增/编辑表单 DTO,对应 Web 管理端公告弹窗提交的表单数据。
|
||||||
|
*
|
||||||
|
* <p><b>表单字段:</b>标题 + 正文 + 状态三字段,是公告维护的最小完备集。
|
||||||
|
* 生效起止时间暂不开放编辑(保持 null),后续如需定时发布再扩展。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AnnouncementSaveDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告标题,列表与详情页标题展示,不可为空。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "公告标题不能为空")
|
||||||
|
@Size(max = 100, message = "公告标题不能超过100个字符")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告正文,点击公告条进入详情页展示,不可为空。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "公告内容不能为空")
|
||||||
|
@Size(max = 5000, message = "公告内容不能超过5000个字符")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示状态:0隐藏 1显示,不可为空。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请选择公告状态")
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
27
src/main/java/com/haisen/laop/dto/AppBannerQueryDTO.java
Normal file
27
src/main/java/com/haisen/laop/dto/AppBannerQueryDTO.java
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图分页查询 DTO,对应 Web 管理端列表筛选条件。
|
||||||
|
*
|
||||||
|
* <p>全部字段可选,GET 参数自动绑定。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AppBannerQueryDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 状态精确匹配:0隐藏 1显示。 */
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/** 页码,0 起(与 Spring Data 约定一致)。 */
|
||||||
|
private Integer pageNo;
|
||||||
|
|
||||||
|
/** 每页数量。 */
|
||||||
|
private Integer pageSize;
|
||||||
|
}
|
||||||
52
src/main/java/com/haisen/laop/dto/AppBannerSaveDTO.java
Normal file
52
src/main/java/com/haisen/laop/dto/AppBannerSaveDTO.java
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图新增/编辑表单 DTO,对应 Web 管理端弹窗提交的表单数据。
|
||||||
|
*
|
||||||
|
* <p><b>说明:</b>title / subtitle 已从表单移除(不再由运营配置),
|
||||||
|
* 由 Service 层在创建时根据跳转类型自动推导默认值,
|
||||||
|
* 或沿用原数据(编辑时)。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AppBannerSaveDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图图片 file_id,不可为空。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请上传轮播图图片")
|
||||||
|
private Long imageFileId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转类型:caac/performance/merchant/pilot/course/custom。
|
||||||
|
*/
|
||||||
|
@Size(max = 32, message = "跳转类型不能超过32个字符")
|
||||||
|
private String linkType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义跳转链接,linkType=custom 时使用。
|
||||||
|
*/
|
||||||
|
@Size(max = 500, message = "自定义链接不能超过500个字符")
|
||||||
|
private String linkUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序值,升序,值越小越靠前。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "排序值不能为空")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示状态:0隐藏 1显示。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请选择展示状态")
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
18
src/main/java/com/haisen/laop/dto/AppConfigChangedEvent.java
Normal file
18
src/main/java/com/haisen/laop/dto/AppConfigChangedEvent.java
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台功能配置变更事件,由 {@code AppConfigServiceImpl.updateValue} 在事务提交后发布。
|
||||||
|
*
|
||||||
|
* <p><b>用途:</b>需要感知配置变更的运行时组件(如微信推送消费者)通过监听此事件
|
||||||
|
* 即时刷新内存状态并唤醒阻塞线程,避免每轮轮询 {@code app_config} 表感知开关变化。</p>
|
||||||
|
*
|
||||||
|
* <p><b>承载信息:</b>仅包含 {@code configKey} 与 {@code configValue},事件本身不携带
|
||||||
|
* "谁改的、何时改的"等审计信息——审计字段已由 {@code app_config.updated_at / updated_by}
|
||||||
|
* 落库,事件只负责"通知变更发生"。</p>
|
||||||
|
*
|
||||||
|
* @param configKey 配置键(如 {@code "mp_push_enabled"})
|
||||||
|
* @param configValue 新配置值(布尔开关为 {@code "0"} 或 {@code "1"})
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
public record AppConfigChangedEvent(String configKey, String configValue) {
|
||||||
|
}
|
||||||
51
src/main/java/com/haisen/laop/dto/CourseAppointmentDTO.java
Normal file
51
src/main/java/com/haisen/laop/dto/CourseAppointmentDTO.java
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程预约提交 DTO,对应小程序课程详情页"立即预约"弹窗表单。
|
||||||
|
*
|
||||||
|
* <p>提交人身份由后端从 token 注入,前端不可伪造;金额由后端按
|
||||||
|
* {@code course.price * personCount} 计算,前端不传金额。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CourseAppointmentDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 课程ID。 */
|
||||||
|
@NotNull(message = "课程ID不能为空")
|
||||||
|
private Long courseId;
|
||||||
|
|
||||||
|
/** 预约时间(具体上课场次)。 */
|
||||||
|
@NotNull(message = "请选择预约时间")
|
||||||
|
private LocalDateTime appointmentDate;
|
||||||
|
|
||||||
|
/** 预约人数,至少 1。 */
|
||||||
|
@NotNull(message = "预约人数不能为空")
|
||||||
|
@Min(value = 1, message = "预约人数至少为1")
|
||||||
|
private Integer personCount;
|
||||||
|
|
||||||
|
/** 联系人姓名。 */
|
||||||
|
@NotBlank(message = "联系人姓名不能为空")
|
||||||
|
@Size(max = 50, message = "联系人姓名不能超过50字")
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
/** 联系人电话。 */
|
||||||
|
@NotBlank(message = "联系电话不能为空")
|
||||||
|
@Size(max = 20, message = "联系电话格式不正确")
|
||||||
|
private String contactPhone;
|
||||||
|
|
||||||
|
/** 备注,选填。 */
|
||||||
|
@Size(max = 200, message = "备注不能超过200字")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
40
src/main/java/com/haisen/laop/dto/CourseQueryDTO.java
Normal file
40
src/main/java/com/haisen/laop/dto/CourseQueryDTO.java
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.constants.CourseConstants;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程列表查询 DTO,对应 Web 管理端课程列表的筛选条件(GET 参数自动绑定)。
|
||||||
|
*
|
||||||
|
* <p><b>全部字段可选:</b>不传即查全量;{@code pageNo} 从 0 开始(与小程序端分页约定一致),
|
||||||
|
* 前端页码由 api 层负责换算。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CourseQueryDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程名称,模糊匹配(LIKE %name%)。
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程状态:0下架 1上架 2已满员,精确匹配;不传查全部。
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码,从 0 开始,默认 0。
|
||||||
|
*/
|
||||||
|
private Integer pageNo = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页条数,默认 10,上限 {@link CourseConstants#MAX_PAGE_SIZE}。
|
||||||
|
*/
|
||||||
|
private Integer pageSize = CourseConstants.DEFAULT_PAGE_SIZE;
|
||||||
|
}
|
||||||
103
src/main/java/com/haisen/laop/dto/CourseSaveDTO.java
Normal file
103
src/main/java/com/haisen/laop/dto/CourseSaveDTO.java
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.constants.CourseConstants;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程新增/编辑请求 DTO,对应 Web 管理端"研学课程"表单的全部字段。
|
||||||
|
*
|
||||||
|
* <p><b>多选字段规约:</b>星期/日期以数组提交,Service 层负责去重、排序后转为
|
||||||
|
* 分号拼接的 {@code String} 存入 DB;禁止前端直接拼字符串提交,避免格式校验遗漏。</p>
|
||||||
|
*
|
||||||
|
* <p><b>排课双模式互斥:</b>{@code scheduleType = 1}(每周重复)时必须提交
|
||||||
|
* {@code scheduleWeekDays};{@code scheduleType = 2}(日期排课)时必须提交
|
||||||
|
* {@code scheduleDates},另一字段自动置空。条件校验在 Service 层完成。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CourseSaveDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程名称,列表/详情页标题。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "课程名称不能为空")
|
||||||
|
@Size(max = 100, message = "课程名称长度不能超过100个字符")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程宣传图片文件ID,关联 sys_file.id;必填。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "课程宣传图片不能为空")
|
||||||
|
private Long coverImageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程售价(元),必填且必须大于 0。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "课程售价不能为空")
|
||||||
|
@DecimalMin(value = "0.01", message = "课程售价必须大于0")
|
||||||
|
private BigDecimal price;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程状态:0下架 1上架(新增/编辑仅允许这两个值,已满员由名额逻辑控制)。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "课程状态不能为空")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程简介,富文本 HTML(支持图片+文字混排),图片经 /api/file/upload 上传后以 URL 内嵌。
|
||||||
|
*/
|
||||||
|
@Size(max = CourseConstants.MAX_DESCRIPTION_LENGTH, message = "课程简介内容过长")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排课方式:1每周重复 2日期排课,必填。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "排课方式不能为空")
|
||||||
|
private Integer scheduleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每周上课星期编码列表(1=周一 ... 7=周日),排课方式为"每周重复"时必填。
|
||||||
|
* Service 层去重升序后转为分号拼接串。
|
||||||
|
*/
|
||||||
|
private List<Integer> scheduleWeekDays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排课日期列表(yyyy-MM-dd),排课方式为"日期排课"时必填。
|
||||||
|
* Service 层校验格式、去重升序后转为分号拼接串。
|
||||||
|
*/
|
||||||
|
private List<String> scheduleDates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系人姓名,必填。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "联系人姓名不能为空")
|
||||||
|
@Size(max = 50, message = "联系人姓名长度不能超过50个字符")
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系电话,必填;中国大陆手机号正则校验。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "联系电话不能为空")
|
||||||
|
@Size(max = 20, message = "联系电话长度不能超过20个字符")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "请输入正确的手机号")
|
||||||
|
private String contactPhone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精选标记:0否 1是。开启后小程序首页"精选研学课程"区块展示;
|
||||||
|
* 非必填,默认 0(新增/编辑表单 el-switch 关闭状态对应 0)。
|
||||||
|
*/
|
||||||
|
private Integer featured;
|
||||||
|
}
|
||||||
59
src/main/java/com/haisen/laop/dto/ExamQuestionSaveDTO.java
Normal file
59
src/main/java/com/haisen/laop/dto/ExamQuestionSaveDTO.java
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 习题新增/编辑 DTO,Web 管理端"培训考试 → 题库"tab 使用。
|
||||||
|
*
|
||||||
|
* <p>前端提交完整题目信息(题干、选项、答案、解析等),后端做基本校验后落库。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExamQuestionSaveDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 编辑时传入,新增时不传。 */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 所属题库ID。 */
|
||||||
|
@NotNull(message = "所属题库不能为空")
|
||||||
|
private Long bankId;
|
||||||
|
|
||||||
|
/** 题目内容。 */
|
||||||
|
@NotBlank(message = "题目内容不能为空")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/** 选项数组:[{"key":"A","val":"..."}],判断题固定传 A=正确、B=错误。 */
|
||||||
|
private List<Map<String, Object>> options;
|
||||||
|
|
||||||
|
/** 题型:1单选 2多选 3判断。 */
|
||||||
|
@NotNull(message = "题型不能为空")
|
||||||
|
private Integer questionType;
|
||||||
|
|
||||||
|
/** 标准答案:单选用 "A",多选用 "AB",判断用 "T"/"F"。 */
|
||||||
|
@NotBlank(message = "正确答案不能为空")
|
||||||
|
private String answer;
|
||||||
|
|
||||||
|
/** 答案解析。 */
|
||||||
|
private String explanation;
|
||||||
|
|
||||||
|
/** 难度:1简单 2中等 3困难。 */
|
||||||
|
@NotNull(message = "难度不能为空")
|
||||||
|
private Integer difficulty;
|
||||||
|
|
||||||
|
/** 分值,默认 5。 */
|
||||||
|
@Positive(message = "分值必须大于0")
|
||||||
|
private Integer score;
|
||||||
|
|
||||||
|
/** 排序号,越小越靠前,默认取当前题库最大 sort_order + 1。 */
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
35
src/main/java/com/haisen/laop/dto/ExamSubmitDTO.java
Normal file
35
src/main/java/com/haisen/laop/dto/ExamSubmitDTO.java
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 答题提交 DTO,用户作答后提交的答案集合。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExamSubmitDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 用户作答列表。 */
|
||||||
|
private List<UserAnswer> answers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单题作答。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class UserAnswer implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 题目ID。 */
|
||||||
|
private Long questionId;
|
||||||
|
|
||||||
|
/** 用户答案:单选"A",多选"AB",判断"T"/"F"。 */
|
||||||
|
private String userAnswer;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/main/java/com/haisen/laop/dto/MerchantCertifyDTO.java
Normal file
68
src/main/java/com/haisen/laop/dto/MerchantCertifyDTO.java
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家认证提交 DTO,对应小程序"商家认证"页面的商家信息表单。
|
||||||
|
*
|
||||||
|
* <p>字段与 {@link com.haisen.laop.entity.Merchant} PO 对齐;服务类型为飞手能力属性,
|
||||||
|
* 已从商家认证中移除(merchant.service_types 历史列保留但不再写入)。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MerchantCertifyDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家名称,对外展示,认证时校验与营业执照一致。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "商家名称不能为空")
|
||||||
|
@Size(max = 100, message = "商家名称长度不能超过100个字符")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家地址,选填。
|
||||||
|
*/
|
||||||
|
@Size(max = 200, message = "商家地址长度不能超过200个字符")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系人姓名。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "联系人不能为空")
|
||||||
|
@Size(max = 50, message = "联系人长度不能超过50个字符")
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系电话,中国大陆手机号正则校验。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "联系电话不能为空")
|
||||||
|
@Size(max = 20, message = "联系电话长度不能超过20个字符")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "请输入正确的手机号")
|
||||||
|
private String contactPhone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家简介,选填,详情页展示。
|
||||||
|
*/
|
||||||
|
@Size(max = 500, message = "商家简介长度不能超过500个字符")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门头照片 file_id,选填。
|
||||||
|
*/
|
||||||
|
private Long doorImageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 营业执照照片 file_id(sys_file.id),认证审核核心材料,必须上传 1 张。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请上传营业执照照片")
|
||||||
|
private Long licenseFileId;
|
||||||
|
}
|
||||||
46
src/main/java/com/haisen/laop/dto/MessageLogQueryDTO.java
Normal file
46
src/main/java/com/haisen/laop/dto/MessageLogQueryDTO.java
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信模板消息日志查询条件(Web 管理端),全部字段可选。
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MessageLogQueryDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人用户 ID,精确筛选。
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务类型编码,精确筛选(1任务申请通知)。
|
||||||
|
*/
|
||||||
|
private Integer bizType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送状态编码,精确筛选(0已跳过 1已提交 2送达成功 3送达失败)。
|
||||||
|
*/
|
||||||
|
private Integer sendStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收人服务号 openid,支持模糊匹配。
|
||||||
|
*/
|
||||||
|
private String openId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码,从 0 开始,默认 0。
|
||||||
|
*/
|
||||||
|
private Integer pageNo = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页条数,默认 10,上限 100。
|
||||||
|
*/
|
||||||
|
private Integer pageSize = 10;
|
||||||
|
}
|
||||||
97
src/main/java/com/haisen/laop/dto/PilotCertifyDTO.java
Normal file
97
src/main/java/com/haisen/laop/dto/PilotCertifyDTO.java
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.constants.CertConstants;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞手认证提交 DTO,对应小程序"飞手认证"页面的表单。
|
||||||
|
*
|
||||||
|
* <p><b>多执照设计:</b>一个飞手可持有多本执照(如同时持有 AOPA 与 UTC),
|
||||||
|
* {@code licenses} 为执照列表,每项包含类型/编号/照片;Service 层按下标
|
||||||
|
* 拼接为分号串后落库(与 {@link com.haisen.laop.entity.Pilot} PO 对齐)。</p>
|
||||||
|
*
|
||||||
|
* <p>认证状态、认证时间等由 Service 层维护,禁止前端提交。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PilotCertifyDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 姓名(实名),认证时与身份证/执照一致。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "姓名不能为空")
|
||||||
|
@Size(max = 50, message = "姓名长度不能超过50个字符")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号,中国大陆手机号正则校验。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Size(max = 20, message = "手机号长度不能超过20个字符")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "请输入正确的手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执照列表,至少 1 本、至多 3 本;嵌套校验每本执照的完整性。
|
||||||
|
*/
|
||||||
|
@Valid
|
||||||
|
@NotEmpty(message = "请至少填写一本执照信息")
|
||||||
|
@Size(max = CertConstants.MAX_LICENSE_COUNT, message = "执照数量最多3本")
|
||||||
|
private List<LicenseItemDTO> licenses;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务类型编码列表(1植保 2吊运 3航拍 4巡检 5清洗 6表演 7测绘),选填。
|
||||||
|
* Service 层转为分号拼接字符串存入 pilot.service_types;编码合法性在 Service 层校验。
|
||||||
|
*/
|
||||||
|
@Size(max = 7, message = "服务类型最多7项")
|
||||||
|
private List<Integer> serviceTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单本执照信息。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class LicenseItemDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAAC 执照类型编码(见 {@code LicenseTypeEnum}:1 AOPA 2 UTC 3 ASFC 4 CAAC)。
|
||||||
|
* 非空仅做存在性校验,编码合法性由 Service 层按枚举精确校验。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请选择执照类型")
|
||||||
|
private Integer licenseType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAAC 执照编号,提交认证时与 CAAC 接口核验真伪。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "执照编号不能为空")
|
||||||
|
@Size(max = 50, message = "执照编号长度不能超过50个字符")
|
||||||
|
private String licenseNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执照照片 file_id(sys_file.id),认证审核核心材料,每本执照必须上传 1 张。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请上传执照照片")
|
||||||
|
private Long licenseFileId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执照获证日期,格式 yyyy-MM-dd(如 2023-06-15);
|
||||||
|
* 与 licenseType / licenseNo / licenseFileId 按下标一一对应。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "请选择获证日期")
|
||||||
|
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "获证日期格式应为 yyyy-MM-dd")
|
||||||
|
private String licenseDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main/java/com/haisen/laop/dto/PointsAdjustDTO.java
Normal file
49
src/main/java/com/haisen/laop/dto/PointsAdjustDTO.java
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.constants.PointsConstants;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web 后台手动增减积分请求体(用户管理 → 积分明细弹窗)。
|
||||||
|
*
|
||||||
|
* <p>方向与数量分离建模:{@code changeType} 决定收入/支出,
|
||||||
|
* {@code points} 统一传正整数(绝对值),符号由后端按方向落库;
|
||||||
|
* 调整原因必填,作为人工账务调整的审计依据写入 {@code points_detail.remark}。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PointsAdjustDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变动方向:1 收入(手动新增) 2 支出(手动扣减),取值见 PointsConstants。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请选择变动类型(新增/扣减)")
|
||||||
|
private Integer changeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 积分数量(正整数,收入/支出的绝对值),区间由 PointsConstants 统一定义。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请填写积分数量")
|
||||||
|
@Min(value = PointsConstants.MIN_MANUAL_ADJUST_POINTS, message = "积分数量必须为正整数")
|
||||||
|
@Max(value = PointsConstants.MAX_MANUAL_ADJUST_POINTS,
|
||||||
|
message = "单次调整积分不能超过 " + PointsConstants.MAX_MANUAL_ADJUST_POINTS)
|
||||||
|
private Integer points;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整原因(必填),写入积分流水备注,便于后续审计溯源。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "请填写调整原因")
|
||||||
|
@Size(max = PointsConstants.MAX_MANUAL_REMARK_LENGTH,
|
||||||
|
message = "调整原因最长 " + PointsConstants.MAX_MANUAL_REMARK_LENGTH + " 字")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
56
src/main/java/com/haisen/laop/dto/TaskHallQueryDTO.java
Normal file
56
src/main/java/com/haisen/laop/dto/TaskHallQueryDTO.java
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抢单大厅筛选查询 DTO,对应小程序任务中心"抢单大厅"页签的筛选条。
|
||||||
|
*
|
||||||
|
* <p><b>区域筛选语义(逐级命中):</b>省市区由微信原生 region Picker 提供,
|
||||||
|
* 三个参数按用户选择的最精确层级生效——选了区县按区县匹配,仅选到市按市匹配,
|
||||||
|
* 仅选到省按省匹配(与 {@code Task} PO 的 province/city/district 名称字段对应)。</p>
|
||||||
|
*
|
||||||
|
* <p><b>金额筛选语义:</b>{@code minReward}/{@code maxReward} 为闭区间边界,
|
||||||
|
* 可单边为空(如"500以上"仅传 minReward)。</p>
|
||||||
|
*
|
||||||
|
* <p>GET 查询参数由 Spring 按字段名自动绑定,全部可选。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskHallQueryDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 省份名称(如"江西省"),任务发布时写入的 province 字段值。
|
||||||
|
*/
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 城市名称(如"九江市"),任务发布时写入的 city 字段值。
|
||||||
|
*/
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区县名称(如"浔阳区"),任务发布时写入的 district 字段值。
|
||||||
|
*/
|
||||||
|
private String district;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务类型编码:1植保 2吊运 3航拍 4巡检 5清洗 6表演 7测绘,空表示不限。
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报酬区间下限(元),null 表示不设下限。
|
||||||
|
*/
|
||||||
|
private BigDecimal minReward;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报酬区间上限(元),null 表示不设上限。
|
||||||
|
*/
|
||||||
|
private BigDecimal maxReward;
|
||||||
|
}
|
||||||
53
src/main/java/com/haisen/laop/dto/TaskManageQueryDTO.java
Normal file
53
src/main/java/com/haisen/laop/dto/TaskManageQueryDTO.java
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import com.haisen.laop.common.constants.TaskConstants;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务管理列表查询 DTO,对应 Web 管理端任务列表的筛选条件(GET 参数自动绑定)。
|
||||||
|
*
|
||||||
|
* <p><b>全部字段可选:</b>不传即查全量;{@code pageNo} 从 0 开始(与小程序端分页约定一致),
|
||||||
|
* 前端页码由 api 层负责换算。</p>
|
||||||
|
*
|
||||||
|
* <p><b>keyword:</b>同时匹配任务编号(task_no)或任务名称(name),模糊查询,
|
||||||
|
* 方便管理员快速检索。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskManageQueryDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索关键词,同时模糊匹配任务编号和任务名称。
|
||||||
|
*/
|
||||||
|
private String keyword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务类型编码(1植保 2吊运 3航拍 4巡检 5清洗 6表演 7测绘),精确匹配;不传查全部。
|
||||||
|
*/
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务类别:0普通任务 1优质任务,精确匹配;不传查全部。
|
||||||
|
*/
|
||||||
|
private Integer isPremium;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务状态(0草稿 1待接单 2已接单 3执行中 4已完成 5已取消),精确匹配;不传查全部。
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页码,从 0 开始,默认 0。
|
||||||
|
*/
|
||||||
|
private Integer pageNo = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每页条数,默认 10,上限 {@link TaskConstants#MAX_SCENE_IMAGE_COUNT} 对应分页上限。
|
||||||
|
*/
|
||||||
|
private Integer pageSize = 10;
|
||||||
|
}
|
||||||
141
src/main/java/com/haisen/laop/dto/TaskPublishDTO.java
Normal file
141
src/main/java/com/haisen/laop/dto/TaskPublishDTO.java
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.*;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布任务请求 DTO,对应原型图"发布任务"页面的全部表单字段。
|
||||||
|
*
|
||||||
|
* <p><b>多选字段规约:</b>前端以数组提交,Service 层负责转为分号拼接的 {@code String} 存入 DB;
|
||||||
|
* 禁止前端直接拼字符串提交,避免格式校验遗漏。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskPublishDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务名称,列表/详情页标题。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "任务名称不能为空")
|
||||||
|
@Size(max = 100, message = "任务名称长度不能超过100个字符")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务类型编码:1植保 2吊运 3航拍 4巡检 5清洗 6表演 7测绘。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "任务类型不能为空")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 省份,任务所在地 LBS 筛选维度。
|
||||||
|
*/
|
||||||
|
@Size(max = 20, message = "省份长度不能超过20个字符")
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 城市,任务所在地 LBS 筛选维度。
|
||||||
|
*/
|
||||||
|
@Size(max = 20, message = "城市长度不能超过20个字符")
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区县。
|
||||||
|
*/
|
||||||
|
@Size(max = 20, message = "区县长度不能超过20个字符")
|
||||||
|
private String district;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详细地址,飞手导航用;必填(地图选点必带)。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "详细地址不能为空")
|
||||||
|
@Size(max = 200, message = "详细地址长度不能超过200个字符")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 纬度坐标,地图选点时自动填入;String 避免浮点精度问题;必填。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "纬度不能为空")
|
||||||
|
@Size(max = 20, message = "纬度长度不能超过20个字符")
|
||||||
|
private String latitude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经度坐标,地图选点时自动填入;String 避免浮点精度问题;必填。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "经度不能为空")
|
||||||
|
@Size(max = 20, message = "经度长度不能超过20个字符")
|
||||||
|
private String longitude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作业场景图 file_id 列表,至少 1 张、最多 5 张。Service 层转为分号拼接字符串。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "场景图不能为空")
|
||||||
|
@Size(min = 1, max = 5, message = "场景图至少上传1张,最多5张")
|
||||||
|
private List<Long> sceneImageIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需求人数,默认 1;团队单/急单可大于 1。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "需求人数不能为空")
|
||||||
|
@Min(value = 1, message = "需求人数至少为1人")
|
||||||
|
@Max(value = 99, message = "需求人数最多99人")
|
||||||
|
private Integer requiredCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务开始时间,精确到分钟。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "开始时间不能为空")
|
||||||
|
private LocalDateTime taskStartTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务结束时间,必填(作业结束日期)。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "结束时间不能为空")
|
||||||
|
private LocalDateTime taskEndTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报酬(元),必填且必须大于 0。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "报酬不能为空")
|
||||||
|
@DecimalMin(value = "0.01", message = "报酬必须大于0")
|
||||||
|
private BigDecimal reward;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保险要求编码列表,Service 层转为分号拼接字符串。
|
||||||
|
* 编码:1=三者险 2=飞手险 3=机身险。可空(无保险要求)。
|
||||||
|
*/
|
||||||
|
private List<Integer> insuranceRequireCodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系人姓名,必填。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "联系人姓名不能为空")
|
||||||
|
@Size(max = 50, message = "联系人姓名长度不能超过50个字符")
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 联系电话,必填;中国大陆手机号正则校验。
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "联系电话不能为空")
|
||||||
|
@Size(max = 20, message = "联系电话长度不能超过20个字符")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "请输入正确的手机号")
|
||||||
|
private String contactPhone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备注/详细任务描述,选填。
|
||||||
|
*/
|
||||||
|
@Size(max = 2000, message = "备注长度不能超过2000个字符")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否优质任务,非必填,默认 false。优质任务消耗 4 积分,金色高亮展示。
|
||||||
|
*/
|
||||||
|
private Boolean isPremium;
|
||||||
|
}
|
||||||
33
src/main/java/com/haisen/laop/dto/TaskReviewDTO.java
Normal file
33
src/main/java/com/haisen/laop/dto/TaskReviewDTO.java
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.haisen.laop.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家审批任务申请请求体。
|
||||||
|
*
|
||||||
|
* <p>同意时 reason 忽略;拒绝时 reason 可选填写(最多 200 字),不填时申请人端
|
||||||
|
* 展示统一文案"商家已拒绝你的申请"。</p>
|
||||||
|
*
|
||||||
|
* @author duhao
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TaskReviewDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批结果:true 同意接单 / false 拒绝。
|
||||||
|
*/
|
||||||
|
@NotNull(message = "请指定审批结果")
|
||||||
|
private Boolean approve;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝原因,仅拒绝时生效,可空。
|
||||||
|
*/
|
||||||
|
@Size(max = 200, message = "拒绝原因最长200字")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user