Compare commits

...

22 Commits

Author SHA1 Message Date
irving
043483a076 docker push不上去,尝试加入github action在服务器构建 2025-10-03 23:25:48 -04:00
huchuansai
29da6b906b fix 2025-09-29 17:58:07 +08:00
huchuansai
e6b71cd897 fix 2025-09-28 09:59:47 +08:00
huchuansai
190f77529a fix 2025-09-27 17:09:26 +08:00
huchuansai
b803e836eb fix: 去æå已经取消的订单隐藏微信号功能 2025-09-24 11:03:12 +08:00
huchuansai
e777adf6b0 feat: 订单催促 2025-09-24 10:49:50 +08:00
huchuansai
e391058b30 fix: code style 2025-09-18 10:06:21 +08:00
huchuansai
099546b0a7 fix: 解决订单状态问题 2025-09-18 10:05:24 +08:00
huchuansai
4e456a3157 fix: 结算 2025-09-15 14:29:20 +08:00
huchuansai
b4d9a0285b 合并 2025-09-13 14:04:56 +08:00
irving
ffcb5eae20 feat: 新增docker部署脚本 2025-09-07 17:21:47 -04:00
irving
4bc5b67937 修复部署失败问题
- 移除SpringSecurityConfig中重复的corsFilter Bean
- 保留CorsConfig中的全局CORS配置
- 解决因Bean定义冲突导致的应用启动失败问题
2025-09-07 17:13:01 -04:00
irving
de54406d19 修复跨域请求问题
- 添加全局CORS配置类CorsConfig.java
- 移除SpringSecurityConfig中的重复CORS配置
- 支持所有域名跨域访问并允许携带凭据
- 解决前端跨域请求被阻止的问题
2025-09-07 17:04:14 -04:00
irving
5a50114b59 Refactor order creation with Builder pattern and type-safe enums
- Replace 20+ parameter method calls with Builder pattern in WxCustomController
- Add RewardType and CommodityType enums with database compatibility
- Update DTOs and service layer for enum conversion
- Fix all test cases with proper enum usage
- Ensure backward compatibility through service layer conversion
2025-09-06 23:20:44 -04:00
irving
295400b83e 重构订单创建逻辑:采用Builder模式替代20+参数方法
主要改进:
- 新增OrderCreationRequest及相关DTO类,使用Builder模式提升代码可读性
- 引入类型安全的枚举类OrderConstant,替代魔法字符串常量
- 重构PlayOrderInfoServiceImpl,新增基于Builder模式的createOrderInfo方法
- 保留原有方法并标记为@Deprecated,确保向后兼容性
- 完善单元测试覆盖,包含Mockito模拟和边界条件测试
- 优化包结构,将DTO类从vo包迁移到dto包
- 添加JUnit 5和Mockito测试依赖
- 移除实体类过度使用的Lombok注解,改用精简的自定义构造器
- 新增数据库开发工作流程文档

技术栈:
- Spring Boot 2.7.9
- MyBatis-Plus 3.5.3.2
- JUnit 5 + Mockito
- Lombok Builder模式
- 类型安全枚举设计
2025-09-06 22:58:14 -04:00
irving
6194c64b4f docs: 添加Docker部署说明文档
- 添加多架构Docker构建和推送说明
- 详细说明服务器部署流程
- 说明CentOS环境下的docker-compose配置
- 添加日志查看和监控指引
- 完善从本地开发到服务器部署的完整工作流程
2025-09-06 22:00:19 -04:00
irving
ea0490e141 feat: 增强Docker构建系统,支持多架构构建
- 为build-docker.sh添加架构特定构建支持 (amd64/arm64)
- 始终使用Docker Buildx确保跨平台构建的一致性
- 添加构建缓存支持,使用.buildx-cache目录
- 更新Dockerfile使用平台感知基础镜像 (FROM --platform=$TARGETPLATFORM)
- 改进push-docker.sh以支持架构特定标签
- 为构建脚本添加帮助选项 (-h)
- 使用更严格的错误处理 (set -euo pipefail)
- 更新.gitignore排除构建缓存

现在支持构建:
- Apple Silicon本地开发 (arm64)
- Linux服务器部署 (amd64)
- 从任意机器进行跨平台构建
2025-09-06 21:57:59 -04:00
irving
b96fdc6427 feat: 增强日志系统和请求追踪功能
- 新增CorrelationFilter过滤器,为每个请求生成唯一跟踪ID
- 增强logback配置,支持关联ID、租户信息和用户ID的结构化日志
- 新增RequestLoggingInterceptor,记录详细的HTTP请求响应信息
2025-09-06 20:30:13 -04:00
irving
dd2342a234 优化构建配置和网络连接设置
- 移除特定平台的JAVE依赖,改用Maven profiles实现跨平台支持
- 更新开发环境配置使用Tailscale网络地址(100.80.201.143)
- 添加macOS Apple Silicon和Linux x86_64的Maven profiles配置
- 升级JAVE版本到3.5.0并优化依赖管理
- 清理代码格式,移除多余的空行导入
2025-09-06 19:42:41 -04:00
irving
102608b85c WIP: 重构Docker构建流程和部署脚本
- 更新Dockerfile为多阶段构建,提升安全性和构建效率
- 重构docker-compose.yml,添加健康检查和网络配置
- 新增Docker镜像构建和推送脚本
- 修复Maven打包配置,移除不必要的配置项
- 添加.dockerignore文件优化构建上下文
- 删除旧的deploy.sh脚本
- 新增JAR部署和回滚脚本

注意:此为开发中版本,请勿部署到生产环境
2025-09-06 19:35:08 -04:00
irving
cd0449d6af WIP 2025-08-29 09:59:08 -04:00
irving
197ca509c5 TODO 2025-08-29 09:58:52 -04:00
48 changed files with 3159 additions and 67556 deletions

59
.dockerignore Normal file
View File

@@ -0,0 +1,59 @@
# Build artifacts and dependencies
target/
.mvn/
*.iml
*.ipr
*.iws
# IDE files
.idea/
.vscode/
*.sublime-*
.eclipse
.project
.classpath
.settings/
# OS files
.DS_Store
Thumbs.db
# Version control
.git/
.gitignore
.gitattributes
# Documentation and scripts (comment out if needed in container)
README.md
*.md
deploy.sh
rollback.sh
fetch-log.sh
# Docker files (avoid recursion)
docker/
Dockerfile*
docker-compose*
# Logs and temp files
log/
logs/
*.log
*.tmp
# Backup files
backup/
*.backup
*.bak
# Environment files
.env
.env.*
# Test files
**/src/test/
# Maven wrapper (we install Maven in container)
.mvn/
mvnw
mvnw.cmd

3
.gitignore vendored
View File

@@ -50,3 +50,6 @@ nbproject/
/var/log/*
*.factorypath
/storage/
# Docker buildx cache
.buildx-cache/

119
CLAUDE.md Normal file
View File

@@ -0,0 +1,119 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Spring Boot multi-module Java backend application called "peipei-backend" that appears to be a social platform/gaming system with WeChat Mini Program integration. The system supports clerk management, customer interactions, orders, gifts, and various social features.
## Project Structure
The project is organized as a Maven multi-module application:
- **play-admin**: Main backend API module containing REST controllers and business logic
- **play-common**: Shared utilities, configurations, and common components
- **play-generator**: Code generation tool for creating CRUD operations from database tables
## Technology Stack
- **Java 11** with Spring Boot 2.5.4
- **MyBatis Plus 3.5.3** with join support for database operations
- **MySQL 8** with Flyway migrations
- **Redis** for caching and session management
- **JWT** for authentication
- **WeChat Mini Program SDK** for WeChat integration
- **Aliyun OSS** for file storage
- **Lombok** for reducing boilerplate code
- **Knife4j/Swagger** for API documentation
## Development Commands
### Building and Running
```bash
# Build the entire project
mvn clean compile
# Package the application
mvn clean package
# Run the main application (play-admin module)
cd play-admin
mvn spring-boot:run
# Or run the packaged jar
java -jar play-admin/target/play-admin-1.0.jar
```
### Database Migrations
```bash
# Run Flyway migrations
mvn flyway:migrate
# Check migration status
mvn flyway:info
```
### Code Generation
```bash
# Generate CRUD code from database tables
cd play-generator
mvn clean compile exec:java
# Or run directly: ./run.sh (Linux/Mac) or run.bat (Windows)
```
## Configuration
The application uses Spring profiles with separate configuration files:
- `application.yml`: Main configuration
- `application-dev.yml`: Development environment
- `application-test.yml`: Test environment
- `application-prod.yml`: Production environment
Default active profile is `test`. Change via `spring.profiles.active` property.
## Architecture
### Module Structure
- **Controllers**: Located in `modules/{domain}/controller/` - Handle HTTP requests and responses
- **Services**: Located in `modules/{domain}/service/` - Business logic layer
- **Mappers**: Located in `modules/{domain}/mapper/` - Database access layer using MyBatis Plus
- **Entities**: Domain objects representing database tables
### Key Domains
- **clerk**: Clerk/staff management and operations
- **custom**: Customer management and interactions
- **order**: Order processing and management
- **shop**: Product catalog and commerce features
- **system**: System administration and user management
- **weichat**: WeChat Mini Program integration
### Authentication & Security
- JWT-based authentication with Spring Security
- Multi-tenant architecture support
- Role-based access control
- XSS protection and input validation
### Database
- Uses MyBatis Plus for ORM with automatic CRUD generation
- Flyway for database migration management
- Logical deletion support (soft delete)
- Multi-tenant data isolation
## Code Generation Tool
The project includes a powerful code generator (`play-generator`) that can:
- Read MySQL table structures
- Generate Entity, Mapper, Service, Controller classes
- Create MyBatis XML mapping files
- Support batch generation for multiple tables
Configure database connection in `play-generator/src/main/resources/config.properties` and specify table names to generate complete CRUD operations.
## Deployment
The project includes a deployment script (`deploy.sh`) that:
- Builds and packages the application
- Deploys to remote server via SCP
- Restarts the application service
Server runs on port 7002 with context path `/api`.

View File

@@ -134,6 +134,76 @@ mvn spotless:apply compile
mvn spotless:apply checkstyle:check compile
```
## 部署说明
### Docker 构建和推送
项目支持多架构 Docker 构建,特别适合在 Apple Silicon Mac 上为 Linux 服务器构建镜像。
#### 构建镜像
```bash
# 构建服务器部署镜像 (Linux amd64)
./build-docker.sh amd64
# 构建本地开发镜像 (Apple Silicon arm64)
./build-docker.sh arm64
# 自动检测架构构建
./build-docker.sh
# 查看帮助
./build-docker.sh -h
```
#### 推送到私有仓库
```bash
# 推送 amd64 镜像到私有仓库 (用于服务器部署)
./push-docker.sh
```
### 服务器部署
#### 部署环境
- **服务器**: CentOS Linux
- **架构**: amd64
- **容器**: Docker + Docker Compose
#### 部署步骤
1. **服务器上的配置文件**
```bash
# 服务器主目录有专门的 docker-compose 文件
~/docker-compose.yml # 为 CentOS 环境优化的配置
```
2. **启动服务**
```bash
# 在服务器主目录执行
cd ~
docker-compose up -d
```
3. **查看日志**
```bash
# 应用日志位置
~/log/ # 应用日志目录
# 查看实时日志
tail -f ~/log/detail.log
tail -f ~/log/error.log
# 查看容器日志
docker-compose logs -f
```
#### 部署文件说明
- **~/docker-compose.yml**: 为 CentOS 环境定制的 Docker Compose 配置
- **~/log/**: 应用日志输出目录
- 配置文件已针对服务器环境进行优化,可直接使用
## 模块介绍
### play-admin

67364
backend.txt

File diff suppressed because it is too large Load Diff

162
build-docker.sh Executable file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# 构建 Docker 镜像脚本
# 用法: ./build-docker.sh [amd64|arm64]
# 如果不指定架构,将使用本机架构
set -euo pipefail
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== 开始构建 PeiPei 后端 Docker 镜像 ===${NC}"
# 检测本机架构
NATIVE_ARCH=$(uname -m)
if [[ "$NATIVE_ARCH" == "x86_64" ]]; then
NATIVE_ARCH="amd64"
elif [[ "$NATIVE_ARCH" == "arm64" ]] || [[ "$NATIVE_ARCH" == "aarch64" ]]; then
NATIVE_ARCH="arm64"
fi
# 处理命令行参数(兼容未提供参数时的 set -u
ARG1="${1-}"
if [[ "$ARG1" == "-h" || "$ARG1" == "--help" ]]; then
echo -e "${GREEN}PeiPei 后端 Docker 镜像构建脚本${NC}"
echo ""
echo -e "${YELLOW}用法:${NC}"
echo " ./build-docker.sh [选项] [架构]"
echo ""
echo -e "${YELLOW}选项:${NC}"
echo " -h, --help 显示此帮助信息"
echo ""
echo -e "${YELLOW}架构:${NC}"
echo " amd64 构建 Linux amd64 镜像 (适用于服务器)"
echo " arm64 构建 Linux arm64 镜像 (适用于 Apple Silicon Mac)"
echo " [空] 自动检测本机架构"
echo ""
echo -e "${YELLOW}示例:${NC}"
echo " ./build-docker.sh # 自动检测架构构建"
echo " ./build-docker.sh amd64 # 构建服务器镜像"
echo " ./build-docker.sh arm64 # 构建 Mac 镜像"
echo " ./build-docker.sh -h # 显示帮助"
echo ""
echo -e "${YELLOW}说明:${NC}"
echo " - 本机架构: ${NATIVE_ARCH}"
echo " - 使用 Docker Buildx 进行构建"
echo " - 镜像将带有架构后缀标签 (如: latest-amd64)"
echo " - 构建缓存存储在: .buildx-cache/"
echo " - 确保 Dockerfile 使用: FROM --platform=\$TARGETPLATFORM"
exit 0
fi
TARGET_ARCH="${ARG1:-$NATIVE_ARCH}"
# 验证架构参数
if [[ "$TARGET_ARCH" != "amd64" && "$TARGET_ARCH" != "arm64" ]]; then
echo -e "${RED}错误: 不支持的架构 '$TARGET_ARCH'${NC}"
echo -e "${YELLOW}支持的架构: amd64, arm64${NC}"
echo -e "${YELLOW}用法: ./build-docker.sh [amd64|arm64]${NC}"
exit 1
fi
echo -e "${YELLOW}目标架构: ${TARGET_ARCH}${NC}"
echo -e "${YELLOW}本机架构: ${NATIVE_ARCH}${NC}"
# 检查是否在正确的目录
if [ ! -f "docker/Dockerfile" ]; then
echo -e "${RED}错误: 请在项目根目录执行此脚本${NC}"
echo -e "${YELLOW}当前目录: $(pwd)${NC}"
echo -e "${YELLOW}期望目录应包含: docker/Dockerfile${NC}"
exit 1
fi
# 获取 UTC+8 时间戳
TIMESTAMP=$(TZ='Asia/Shanghai' date +"%Y-%m-%d-%Hh-%Mm")
echo -e "${YELLOW}构建时间戳 (UTC+8): ${TIMESTAMP}${NC}"
# 镜像名称和标签
IMAGE_NAME="peipei-backend"
VERSION_TAG="${TIMESTAMP}-${TARGET_ARCH}"
LATEST_TAG="latest-${TARGET_ARCH}"
echo -e "${YELLOW}镜像名称: ${IMAGE_NAME}${NC}"
echo -e "${YELLOW}版本标签: ${VERSION_TAG}${NC}"
# 构建 Docker 镜像
echo -e "${GREEN}开始构建镜像...${NC}"
# 确保 buildx 可用
if ! docker buildx version >/dev/null 2>&1; then
echo -e "${RED}错误: 需要 Docker Buildx 支持${NC}"
exit 1
fi
# 创建并使用 builder如果不存在
if ! docker buildx inspect peipei-builder >/dev/null 2>&1; then
echo -e "${YELLOW}创建 buildx builder...${NC}"
docker buildx create --name peipei-builder --use
else
docker buildx use peipei-builder
fi
# 启动 builder 和 QEMU用于跨架构支持
echo -e "${YELLOW}初始化 buildx builder...${NC}"
docker buildx inspect --bootstrap >/dev/null
# 显示构建类型
if [[ "$TARGET_ARCH" != "$NATIVE_ARCH" ]]; then
echo -e "${YELLOW}跨平台构建: ${NATIVE_ARCH} -> ${TARGET_ARCH}${NC}"
else
echo -e "${YELLOW}本机架构构建: ${TARGET_ARCH}${NC}"
fi
# 创建缓存目录(可选优化)
CACHE_DIR=".buildx-cache"
mkdir -p "$CACHE_DIR"
# 始终使用 buildx 以保持行为一致
echo -e "${GREEN}执行构建...${NC}"
if docker buildx build \
--platform "linux/${TARGET_ARCH}" \
--load \
--cache-from="type=local,src=${CACHE_DIR}" \
--cache-to="type=local,dest=${CACHE_DIR}" \
-f docker/Dockerfile \
-t "${IMAGE_NAME}:${VERSION_TAG}" \
-t "${IMAGE_NAME}:${LATEST_TAG}" \
.; then
BUILD_SUCCESS=true
else
BUILD_SUCCESS=false
fi
# 检查构建结果
if [[ "$BUILD_SUCCESS" == "true" ]]; then
echo -e "${GREEN}✅ Docker 镜像构建成功!${NC}"
echo -e "${GREEN}镜像标签:${NC}"
echo -e " - ${IMAGE_NAME}:${VERSION_TAG}"
echo -e " - ${IMAGE_NAME}:${LATEST_TAG}"
echo -e "\n${YELLOW}镜像信息:${NC}"
docker images | grep -E "^${IMAGE_NAME}\s"
echo -e "\n${GREEN}使用说明:${NC}"
if [[ "$TARGET_ARCH" == "amd64" ]]; then
echo -e " - 该镜像适用于 Linux amd64 服务器部署"
echo -e " - 推送到服务器: ./push-docker.sh"
elif [[ "$TARGET_ARCH" == "arm64" ]]; then
echo -e " - 该镜像适用于 Apple Silicon Mac 本地运行"
echo -e " - 本地测试: docker run ${IMAGE_NAME}:${LATEST_TAG}"
fi
echo -e "\n${YELLOW}构建其他架构:${NC}"
echo -e " - 构建 amd64 (服务器): ./build-docker.sh amd64"
echo -e " - 构建 arm64 (Mac): ./build-docker.sh arm64"
echo -e " - 自动检测架构: ./build-docker.sh"
else
echo -e "${RED}❌ Docker 镜像构建失败!${NC}"
exit 1
fi

219
db_workflow.md Normal file
View File

@@ -0,0 +1,219 @@
# Database Development Workflow
This document outlines the recommended workflow for database schema changes and code generation in the peipei-backend project.
## Overview
The project uses a **Database-First** approach with the following flow:
```
Flyway Migration → Database Schema → Code Generator → Java Code
```
## Step-by-Step Workflow
### 1. Create Flyway Migration
Create a new migration file in `/play-admin/src/main/resources/db/migration/`:
```sql
-- V{version}__{description}.sql
-- Example: V2__add_new_feature_table.sql
CREATE TABLE `play_new_feature` (
`id` varchar(32) NOT NULL COMMENT 'UUID',
`feature_name` varchar(100) COMMENT '功能名称',
`feature_type` varchar(20) DEFAULT NULL COMMENT '功能类型',
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人的id',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '修改人的id',
`updated_time` datetime DEFAULT NULL COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除',
`version` int(11) NOT NULL DEFAULT '1' COMMENT '数据版本',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='新功能表';
```
**Migration Naming Convention:**
- `V{version}__{description}.sql`
- Version format: `V1.2025.0609.10.11` (timestamp-based)
- Description: snake_case with double underscore
### 2. Run Flyway Migration
Apply the migration to update database schema:
```bash
# Run from project root
mvn flyway:migrate
# Or check migration status
mvn flyway:info
```
### 3. Configure Code Generator
Edit `/play-generator/src/main/resources/config.properties`:
```properties
# Database configuration (should match main app)
db.url=jdbc:mysql://localhost:3306/play-with?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
db.driver=com.mysql.cj.jdbc.Driver
db.username=root
db.password=your_password
# Code generation configuration
gen.author=your_name
gen.packageName=com.starry.admin
gen.outputDir=./generated-code
gen.autoRemovePre=false
gen.tablePrefix=play_
gen.tplCategory=crud
# Specify the new table(s) to generate
gen.tableNames=play_new_feature
```
### 4. Run Code Generator
Generate Java code from the new table structure:
```bash
cd play-generator
./run.sh
# Or manually: mvn clean compile exec:java
```
### 5. Review Generated Code
Check the generated files in `./generated-code/`:
```
generated-code/
├── src/main/java/com/starry/admin/
│ ├── entity/PlayNewFeatureEntity.java
│ ├── mapper/PlayNewFeatureMapper.java
│ ├── service/IPlayNewFeatureService.java
│ ├── service/impl/PlayNewFeatureServiceImpl.java
│ └── controller/PlayNewFeatureController.java
└── src/main/resources/mapper/PlayNewFeatureMapper.xml
```
### 6. Integrate Generated Code
Copy generated files to the appropriate module in play-admin:
```bash
# Create new module directory structure
mkdir -p play-admin/src/main/java/com/starry/admin/modules/newfeature/{controller,service,mapper,module/entity}
# Copy generated files to appropriate locations
cp generated-code/src/main/java/com/starry/admin/controller/* \
play-admin/src/main/java/com/starry/admin/modules/newfeature/controller/
cp generated-code/src/main/java/com/starry/admin/service/* \
play-admin/src/main/java/com/starry/admin/modules/newfeature/service/
cp generated-code/src/main/java/com/starry/admin/mapper/* \
play-admin/src/main/java/com/starry/admin/modules/newfeature/mapper/
cp generated-code/src/main/java/com/starry/admin/entity/* \
play-admin/src/main/java/com/starry/admin/modules/newfeature/module/entity/
cp generated-code/src/main/resources/mapper/* \
play-admin/src/main/resources/mapper/newfeature/
```
### 7. Customize and Test
- **Review generated code** for any needed customizations
- **Add business logic** to Service implementations
- **Customize validation** in Controllers
- **Add custom queries** in Mapper if needed
- **Test the new endpoints** via Swagger UI
- **Run application** to ensure everything works
## Database Design Best Practices
### Table Naming Convention
- Use `play_` prefix for all tables
- Use snake_case for table names
- Example: `play_order_info`, `play_clerk_level_info`
### Required Standard Columns
All tables should include these standard columns:
```sql
`id` varchar(32) NOT NULL COMMENT 'UUID',
`tenant_id` varchar(32) NOT NULL COMMENT '租户ID',
`created_by` varchar(32) DEFAULT NULL COMMENT '创建人的id',
`created_time` datetime DEFAULT NULL COMMENT '创建时间',
`updated_by` varchar(32) DEFAULT NULL COMMENT '修改人的id',
`updated_time` datetime DEFAULT NULL COMMENT '修改时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除',
`version` int(11) NOT NULL DEFAULT '1' COMMENT '数据版本',
```
### Column Guidelines
- Always add meaningful `COMMENT` to columns
- Use appropriate data types (`varchar`, `int`, `decimal`, `datetime`)
- Set proper defaults where applicable
- Consider indexing for foreign keys and frequently queried columns
## Code Generation Notes
### What Gets Generated
- **Entity**: MyBatis Plus entity with Lombok annotations
- **Mapper**: Database access interface extending BaseMapper
- **Service**: Business logic interface with standard CRUD operations
- **ServiceImpl**: Service implementation with basic CRUD logic
- **Controller**: REST API endpoints with Swagger documentation
- **Mapper XML**: MyBatis SQL mapping files
### Generated Code Features
- Swagger API documentation
- Spring Security integration (`@PreAuthorize`)
- Audit logging (`@Log`)
- Input validation
- Pagination support
- Logical deletion support
- Multi-tenant support
## Troubleshooting
### Common Issues
**Migration Fails**:
- Check database connection settings
- Verify migration SQL syntax
- Ensure proper permissions
**Generator Fails**:
- Verify database connection in config.properties
- Check if table exists after migration
- Ensure table follows naming conventions
**Generated Code Issues**:
- Review column comments (used for Java doc)
- Check data type mappings
- Verify package naming in config
### Database Type Mappings
| MySQL Type | Java Type | Notes |
|------------|-----------|-------|
| `varchar(n)` | `String` | |
| `int`, `bigint` | `Integer`, `Long` | |
| `decimal(p,s)` | `BigDecimal` | Precise decimal calculations |
| `datetime` | `Date` | Or `LocalDateTime` |
| `tinyint(1)` | `Boolean` | For flags/status |
| `char(1)` | `String` | For status codes |
## Tips
1. **Always create migration first**, then generate code
2. **Use descriptive table and column names** - they become class and field names
3. **Test migrations on dev environment** before applying to production
4. **Review generated code** before committing - customize as needed
5. **Follow existing module patterns** when organizing generated code
6. **Backup database** before running migrations in production

16
deploy-docker.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Docker deployment script
set -e
# Get current time and format it
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "Deployment started at: $current_time"
echo "Connecting to server to update docker container..."
# SSH into the server, pull the latest image and restart the container
ssh root@122.51.20.105 "source /etc/profile; cd ~; docker compose pull && docker compose up -d"
echo "Docker container updated!"
# Get current time and format it
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "Deployment finished at: $current_time"

23
deploy-jar.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
# 发包脚本
set -e
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "发布开始,当前时间是:$current_time"
# 构建项目
echo "开始构建项目..."
mvn clean package -DskipTests
echo "构建完成!"
# 备份当前的jar文件
ssh root@122.51.20.105 "source /etc/profile; cd /www/wwwroot/july.hucs.top; if [ -f play-admin-1.0.jar ]; then mv play-admin-1.0.jar play-admin-1.0.jar.backup; fi"
echo "备份完成!"
scp ./play-admin/target/play-admin-1.0.jar root@122.51.20.105:/www/wwwroot/july.hucs.top
echo "上传成功!"
ssh root@122.51.20.105 "source /etc/profile;cd /www/wwwroot/july.hucs.top;sh start.sh restart"
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "发布完成,当前时间是:$current_time"

View File

@@ -1,11 +0,0 @@
#!/bin/sh
# 发包脚本
set -e
echo "发布开始,当前时间是:$current_time"
#mvn clean install
scp ./play-admin/target/play-admin-1.0.jar root@122.51.20.105:/www/wwwroot/july.hucs.top
ssh root@122.51.20.105 "source /etc/profile;cd /www/wwwroot/july.hucs.top;sh start.sh restart"
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "发布完成,当前时间是:$current_time"

View File

@@ -1,16 +1,38 @@
FROM openjdk:11-jre-slim
# ---------- Build stage ----------
FROM maven:3.9-eclipse-temurin-11 AS build
WORKDIR /workspace
# 设置工作目录
# Cache-friendly: copy POMs first
COPY pom.xml .
COPY play-admin/pom.xml play-admin/
COPY play-common/pom.xml play-common/
COPY play-generator/pom.xml play-generator/
# Warm deps cache
RUN --mount=type=cache,target=/root/.m2 mvn -B -DskipTests dependency:go-offline
# Add sources
COPY play-admin/src play-admin/src/
COPY play-common/src play-common/src/
COPY play-generator/src play-generator/src/
# Build only app module (and its deps)
RUN --mount=type=cache,target=/root/.m2 mvn -pl play-admin -am -B -DskipTests -T 1C clean package
# ---------- Runtime stage (multi-arch) ----------
FROM --platform=$TARGETPLATFORM eclipse-temurin:11-jre AS runtime
# non-root
RUN groupadd -g 1001 appgroup && useradd -u 1001 -g appgroup -m -s /usr/sbin/nologin appuser
WORKDIR /app
# 只复制应用程序JAR包不复制lib目录
COPY ./*.jar app.jar
COPY --from=build /workspace/play-admin/target/*.jar /app/app.jar
RUN chown -R appuser:appgroup /app
USER appuser
# 设置环境变量
ENV APP_NAME=app.jar
ENV JAVA_OPTS="-Xms2g -Xmx2g" \
SPRING_PROFILES_ACTIVE="test" \
TZ="Asia/Shanghai" \
LANG="C.UTF-8"
# 暴露应用端口
EXPOSE 8080
# 设置启动命令
CMD ["sh", "-c", "java -Dloader.path=./lib/ -Xms2g -Xmx2g -jar $APP_NAME --spring.profiles.active=test"]
EXPOSE 7002
ENTRYPOINT ["sh","-c","exec java $JAVA_OPTS -jar /app/app.jar --spring.profiles.active=${SPRING_PROFILES_ACTIVE}"]

30
docker/README.md Normal file
View File

@@ -0,0 +1,30 @@
# 私有 Docker 仓库使用速查表
# 仓库地址: https://docker-registry.julyhaven.com
# 用户名: hucs
# 密码: hucsdev
# 登录
docker login docker-registry.julyhaven.com -u hucs -p hucsdev
# 推送镜像 (示例: myapp:1.0.0)
docker tag myapp:1.0.0 docker-registry.julyhaven.com/myteam/myapp:1.0.0
docker push docker-registry.julyhaven.com/myteam/myapp:1.0.0
# 同时推送 latest
docker tag myapp:1.0.0 docker-registry.julyhaven.com/myteam/myapp:latest
docker push docker-registry.julyhaven.com/myteam/myapp:latest
# 拉取镜像
docker pull docker-registry.julyhaven.com/myteam/myapp:1.0.0
docker pull docker-registry.julyhaven.com/myteam/myapp:latest
# 测试镜像 (hello-world)
docker pull hello-world
docker tag hello-world docker-registry.julyhaven.com/test/hello-world:latest
docker push docker-registry.julyhaven.com/test/hello-world:latest
docker pull docker-registry.julyhaven.com/test/hello-world:latest
# 注意事项:
# 1. latest 会被覆盖,生产环境请用版本号标签
# 2. 已支持大镜像推送/拉取,无需额外配置
# 3. 登录失败时请确认用户名/密码是否正确

View File

@@ -1,12 +1,37 @@
version: '3'
version: "3.8"
services:
spring-boot-app:
build: .
container_name: spring-boot-app
peipei-backend:
image: docker-registry.julyhaven.com/peipei/backend:latest
container_name: peipei-backend
ports:
- "7003:7002"
environment:
- SPRING_PROFILES_ACTIVE=test
- JAVA_OPTS=-Xms2g -Xmx2g
volumes:
- ./lib:/app/lib # 挂载主机的lib目录到容器内的lib目录
- ./log:/app/log # 挂载日志目录到主机
restart: always
- ./log:/app/log # Mount log directory to host
restart: unless-stopped
healthcheck:
# Maybe use actuator later
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:7002/api/health",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- peipei-network
networks:
peipei-network:
driver: bridge
# volumes:
# mysql_data:

View File

@@ -1,18 +0,0 @@
#!/bin/sh
# 拉取日志脚本
set -e
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "开始拉取日志,当前时间是:$current_time"
# 创建本地log目录如果不存在
mkdir -p ./log
# 从远程服务器拉取整个log文件夹
echo "正在从远程服务器拉取日志文件..."
scp -r root@122.51.20.105:/www/wwwroot/july.hucs.top/log/* ./log/
# 获取当前时间并格式化为指定格式
current_time=$(date +"%Y-%m-%d %H:%M:%S")
echo "日志拉取完成,当前时间是:$current_time"

View File

@@ -93,9 +93,17 @@
<artifactId>jave-core</artifactId>
</dependency>
<!-- Include native ffmpeg binaries for common runtimes to avoid
build-platform vs target-platform mismatches in Docker builds. -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osxm1</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
@@ -134,6 +142,23 @@
<version>0.2.0</version>
</dependency>
<!--JUnit 5 for testing-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -157,23 +182,22 @@
<artifactId>flyway-maven-plugin</artifactId>
<version>7.15.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.9</version>
<configuration>
<layout>ZIP</layout>
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
<goal>repackage</goal>
</goals>
</execution>
</executions>

View File

@@ -0,0 +1,64 @@
package com.starry.admin.common.config;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
*
* @author admin
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.exposedHeaders("*")
.maxAge(3600);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 允许所有域名进行跨域调用
configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
// 允许所有请求头
configuration.setAllowedHeaders(Collections.singletonList("*"));
// 允许所有HTTP方法
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"));
// 允许发送Cookie
configuration.setAllowCredentials(true);
// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
configuration.setExposedHeaders(Arrays.asList("*"));
// 预检请求的缓存时间(秒)
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public CorsFilter corsFilter() {
return new CorsFilter(corsConfigurationSource());
}
}

View File

@@ -0,0 +1,207 @@
package com.starry.admin.common.filter;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.impl.PlayCustomUserInfoServiceImpl;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.common.context.CustomSecurityContextHolder;
import java.io.IOException;
import java.util.UUID;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 请求关联ID过滤器为每个HTTP请求生成唯一的跟踪ID
* 用于日志关联和请求链路追踪
*
* @author Claude
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationFilter implements Filter {
@Resource
@Lazy
private WxTokenService wxTokenService;
@Resource
@Lazy
private PlayClerkUserInfoServiceImpl clerkUserInfoService;
@Resource
@Lazy
private PlayCustomUserInfoServiceImpl customUserInfoService;
public static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
public static final String CORRELATION_ID_MDC_KEY = "correlationId";
public static final String USER_ID_MDC_KEY = "userId";
public static final String TENANT_ID_MDC_KEY = "tenantId";
public static final String TENANT_KEY_MDC_KEY = "tenantKey";
public static final String REQUEST_URI_MDC_KEY = "requestUri";
public static final String REQUEST_METHOD_MDC_KEY = "requestMethod";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
// 生成或获取关联ID
String correlationId = getOrGenerateCorrelationId(httpRequest);
// 设置MDC上下文
MDC.put(CORRELATION_ID_MDC_KEY, correlationId);
MDC.put(REQUEST_URI_MDC_KEY, httpRequest.getRequestURI());
MDC.put(REQUEST_METHOD_MDC_KEY, httpRequest.getMethod());
// 提取并设置租户信息
String tenantKey = httpRequest.getHeader("tenantkey");
if (tenantKey != null && !tenantKey.trim().isEmpty()) {
MDC.put(TENANT_KEY_MDC_KEY, tenantKey);
}
// 尝试从安全上下文获取租户ID
try {
String tenantId = CustomSecurityContextHolder.getTenantId();
if (tenantId != null && !tenantId.trim().isEmpty() && !"9999".equals(tenantId)) {
MDC.put(TENANT_ID_MDC_KEY, tenantId);
}
} catch (Exception e) {
// 忽略获取租户ID的异常
}
// 尝试获取用户ID可能来自JWT或其他认证信息
String userId = extractUserId(httpRequest);
if (userId != null) {
MDC.put(USER_ID_MDC_KEY, userId);
}
// 将关联ID添加到响应头
httpResponse.setHeader(CORRELATION_ID_HEADER, correlationId);
// 继续过滤器链
chain.doFilter(request, response);
} finally {
// 清理MDC上下文
MDC.clear();
}
}
/**
* 获取或生成关联ID
* 优先从请求头获取,如果没有则生成新的
*/
private String getOrGenerateCorrelationId(HttpServletRequest request) {
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.trim().isEmpty()) {
correlationId = "REQ-" + UUID.randomUUID().toString().substring(0, 8);
}
return correlationId;
}
/**
* 尝试从请求中提取用户ID
* 根据项目的认证机制:支持微信端(clerk/custom token)和管理端(Authorization header)
*/
private String extractUserId(HttpServletRequest request) {
try {
// 1. 微信端 - Clerk用户认证
String clerkToken = request.getHeader("clerkusertoken");
if (clerkToken != null && !clerkToken.trim().isEmpty()) {
return extractUserIdFromWxToken(clerkToken, "clerk");
}
// 2. 微信端 - Custom用户认证
String customToken = request.getHeader("customusertoken");
if (customToken != null && !customToken.trim().isEmpty()) {
return extractUserIdFromWxToken(customToken, "custom");
}
// 3. 管理端 - JWT Bearer token认证
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return extractUserIdFromJwtToken(authHeader);
}
} catch (Exception e) {
// 如果解析失败不影响主流程只是记录日志时没有用户ID
// 可以选择记录debug日志但不抛异常
}
return null;
}
/**
* 从微信token中提取真实用户ID
*/
private String extractUserIdFromWxToken(String token, String userType) {
try {
// 使用WxTokenService解析JWT token获取真实用户ID
String userId = wxTokenService.getWxUserIdByToken(token);
if (userId != null) {
// 根据用户类型获取更多用户信息(可选)
if ("clerk".equals(userType)) {
PlayClerkUserInfoEntity clerkUser = clerkUserInfoService.selectById(userId);
if (clerkUser != null) {
// 返回格式: clerk_userId 或者可以包含昵称等信息
return "clerk_" + userId;
}
} else if ("custom".equals(userType)) {
PlayCustomUserInfoEntity customUser = customUserInfoService.selectById(userId);
if (customUser != null) {
return "custom_" + userId;
}
}
// 如果查询用户详情失败至少返回基础用户ID
return userType + "_" + userId;
}
} catch (Exception e) {
// Token解析失败可能是过期或无效token不影响主流程
}
return null;
}
/**
* 从管理端JWT token中提取用户ID
*/
private String extractUserIdFromJwtToken(String authHeader) {
try {
// 管理端的JWT解析比较复杂这里先返回标识
// 实际应该通过JwtToken组件或SecurityUtils获取当前登录用户信息
return "admin_user";
} catch (Exception e) {
return null;
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化逻辑
}
@Override
public void destroy() {
// 清理逻辑
}
}

View File

@@ -79,9 +79,9 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
}
}
} catch (Exception ignored) {
if (SecurityUtils.isLogin()) {
return SecurityUtils.getUserId();
}
// if (SecurityUtils.isLogin()) {
// return SecurityUtils.getUserId();
// }
}
return "";
}

View File

@@ -24,9 +24,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* @author admin
@@ -62,7 +59,7 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
.antMatchers("/health", "/health/**").permitAll()
// 跨域请求会先进行一次options请求
.antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated().and().cors().configurationSource(this.corsConfigurationSource());
.authenticated().and().cors();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加Logout filter
@@ -74,18 +71,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
.authenticationEntryPoint(customAuthenticationEntryPoint);
}
private CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedHeader("*"); // 这个得加上一些复杂的请求方式会带有header不加上跨域会失效。
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("*");
corsConfiguration.addAllowedOriginPattern("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());

View File

@@ -1,5 +1,7 @@
package com.starry.admin.common.task;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkWagesDetailsInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkWagesInfoEntity;
@@ -18,6 +20,8 @@ import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
@@ -26,11 +30,10 @@ import org.springframework.stereotype.Component;
* @author admin
* @since 2024/5/28 下午2:42
**/
@Slf4j
@Component
public class ClerkWagesSettlementTask {
@Resource
ISysTenantService sysTenantService;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@@ -47,31 +50,33 @@ public class ClerkWagesSettlementTask {
/**
* 每分钟查询未结算订单 如果订单完成时间超过24小时可进行结算生成工资
*/
// @Scheduled(cron = "0 0/1 * * * ?")
@Scheduled(cron = "0 0/10 * * * ?")
public void dailyRanking() {
// 1、查询所有的租户信息
List<SysTenantEntity> tenantEntities = sysTenantService.listAll();
for (SysTenantEntity tenantEntity : tenantEntities) {
SecurityUtils.setTenantId(tenantEntity.getTenantId());
List<PlayClerkUserInfoEntity> clerkUserReturnVos = clerkUserInfoService.listAll();
// 生成每个人的工资信息
for (PlayClerkUserInfoEntity clerkUserInfo : clerkUserReturnVos) {
List<PlayClerkUserInfoEntity> clerkUserReturnVos = clerkUserInfoService.listAll();
// 生成每个人的工资信息
for (PlayClerkUserInfoEntity clerkUserInfo : clerkUserReturnVos) {
try {
updateClerkWagesInfo(clerkUserInfo.getId());
} catch (Exception e) {
log.error("订单结算异常:" + e.getMessage());
log.error(e.getMessage(),e);
}
}
}
/**
* 活动24小时前完成的订单对订单进行结算
*
* @param clerkId
* 店员ID
* @param clerkId 店员ID
*/
public void updateClerkWagesInfo(String clerkId) {
// 获得24小时前完成,并且未结算的订单
LocalDateTime lastTime = LocalDateTime.now().minusDays(1);
// 查询当前店员上次结算工资到现在的订单
List<PlayOrderInfoEntity> orderInfoEntities = playOrderInfoService.listByEndTime(clerkId, lastTime);
if(CollectionUtils.isEmpty(orderInfoEntities)) return;
PlayClerkWagesInfoEntity entity = playClerkWagesInfoService.getLastSettlement(clerkId);
// 更新订单信息
updateClerkWagesInfo(clerkId, entity == null ? IdUtils.getUuid() : entity.getId(), orderInfoEntities);
@@ -80,8 +85,7 @@ public class ClerkWagesSettlementTask {
/**
* 更新最新一次工资统计信息 更新订单记录
*
* @param orderInfoEntities
* 订单列表
* @param orderInfoEntities 订单列表
*/
public void updateClerkWagesInfo(String clerkId, String wagesId, List<PlayOrderInfoEntity> orderInfoEntities) {
// 修改订单状态并且新增订单结算详情
@@ -89,7 +93,10 @@ public class ClerkWagesSettlementTask {
// 修改订单状态
orderInfo.setOrderSettlementState("1");
orderInfo.setOrderSettlementTime(LocalDateTime.now());
playOrderInfoService.update(orderInfo);
playOrderInfoService.update(Wrappers.lambdaUpdate(PlayOrderInfoEntity.class)
.eq(PlayOrderInfoEntity::getId, orderInfo.getId())
.set(PlayOrderInfoEntity::getOrderSettlementState, "1")
.set(PlayOrderInfoEntity::getOrderSettlementTime, LocalDateTime.now()));
PlayClerkWagesDetailsInfoEntity wagesDetailsInfo = playClerkWagesDetailsInfoService
.selectByOrderId(orderInfo.getId());
@@ -103,6 +110,7 @@ public class ClerkWagesSettlementTask {
wagesDetailsInfo.setOrderId(orderInfo.getId());
wagesDetailsInfo.setOrderNo(orderInfo.getOrderNo());
wagesDetailsInfo.setFinalAmount(orderInfo.getFinalAmount());
wagesDetailsInfo.setTenantId(orderInfo.getTenantId());
wagesDetailsInfo.setEstimatedRevenue(orderInfo.getEstimatedRevenue());
wagesDetailsInfo.setEndOrderTime(orderInfo.getOrderEndTime());
playClerkWagesDetailsInfoService.saveOrUpdate(wagesDetailsInfo);
@@ -138,6 +146,7 @@ public class ClerkWagesSettlementTask {
wagesInfo.setFinalAmount(finalAmount);
wagesInfo.setOrderContinueNumber(orderContinueNumber);
wagesInfo.setOrderContinueMoney(orderContinueMoney);
wagesInfo.setTenantId(orderInfoEntities.get(0).getTenantId());
wagesInfo.setOrdersExpiredNumber(ordersExpiredNumber);
wagesInfo.setSettlementDate(LocalDate.now());
wagesInfo.setEstimatedRevenue(estimatedRevenue);

View File

@@ -72,12 +72,9 @@ public class OrderRankingSettlementTask {
/**
* 生产排行信息 1、查询所有的租户信息 2、查询每个租户的用户以及他们在对应时间段的订单 3、根据订单生产排行信息
*
* @param startTime
* 接单开始时间
* @param endTime
* 接单截至时间
* @param weeklyRanking
* weeklyRanking 日排名还是周排名0:每日排名;1:每周排名)
* @param startTime 接单开始时间
* @param endTime 接单截至时间
* @param weeklyRanking weeklyRanking 日排名还是周排名0:每日排名;1:每周排名)
* @author admin
* @since 2024/6/12 15:21
**/
@@ -95,8 +92,9 @@ public class OrderRankingSettlementTask {
// 生成每个人的订单排行信息
for (PlayClerkUserInfoEntity clerkUserInfo : clerkUserReturnVos) {
// 生成订单排行数据
clerkRankingInfoEntities.add(
getClerkRanking(clerkUserInfo.getId(), startTime, endTime, weeklyRanking, newSerialNumber));
PlayClerkRankingInfoEntity clerkRanking = getClerkRanking(clerkUserInfo.getId(), startTime, endTime, weeklyRanking, newSerialNumber);
clerkRanking.setTenantId(tenantEntity.getTenantId());
clerkRankingInfoEntities.add(clerkRanking);
}
// 更新排行名次
clerkRankingInfoEntities.sort((p1, p2) -> p2.getOrderNumber() - p1.getOrderNumber());
@@ -104,8 +102,7 @@ public class OrderRankingSettlementTask {
PlayClerkRankingInfoEntity item = clerkRankingInfoEntities.get(i);
item.setRankingIndex(i + 1);
if (i > 0) {
item.setPreviousMoney(
clerkRankingInfoEntities.get(i - 1).getOrderMoney().subtract(item.getOrderMoney()));
item.setPreviousMoney(clerkRankingInfoEntities.get(i - 1).getOrderMoney().subtract(item.getOrderMoney()));
}
}
// 排名数据生成后,将以往排名状态标记为历史排名
@@ -118,21 +115,15 @@ public class OrderRankingSettlementTask {
/**
* 生产当前排行信息
*
* @param clerkId
* 店员ID
* @param startTime
* 排行开始统计日期 yyyy-mm-dd 00:00:00
* @param endTime
* 排行结束统计日期 yyyy-mm-dd 23:59:59
* @param weeklyRanking
* 日排名还是周排名0:每日排名;1:每周排名)
* @param serialNumber
* 查询当前租户、当前排行、的最后一次统计编号
* @param clerkId 店员ID
* @param startTime 排行开始统计日期 yyyy-mm-dd 00:00:00
* @param endTime 排行结束统计日期 yyyy-mm-dd 23:59:59
* @param weeklyRanking 日排名还是周排名0:每日排名;1:每周排名)
* @param serialNumber 查询当前租户、当前排行、的最后一次统计编号
* @author admin
* @since 2024/6/7 11:43
**/
public PlayClerkRankingInfoEntity getClerkRanking(String clerkId, String startTime, String endTime,
String weeklyRanking, Integer serialNumber) {
public PlayClerkRankingInfoEntity getClerkRanking(String clerkId, String startTime, String endTime, String weeklyRanking, Integer serialNumber) {
List<PlayOrderInfoEntity> orderInfoEntities = orderInfoService.listByTime(clerkId, startTime, endTime, null);
int orderNumber = orderInfoEntities.size();
BigDecimal orderMoney = BigDecimal.ZERO;
@@ -151,13 +142,8 @@ public class OrderRankingSettlementTask {
ordersExpiredNumber++;
}
}
BigDecimal orderContinueProportion = orderNumber == 0
? BigDecimal.ZERO
: new BigDecimal(ordersExpiredNumber).divide(new BigDecimal(orderNumber), 4, RoundingMode.HALF_UP)
.add(new BigDecimal(100));
BigDecimal averageUnitPrice = customIds.isEmpty()
? BigDecimal.ZERO
: orderMoney.divide(new BigDecimal(customIds.size()), 4, RoundingMode.HALF_UP);
BigDecimal orderContinueProportion = orderNumber == 0 ? BigDecimal.ZERO : new BigDecimal(ordersExpiredNumber).divide(new BigDecimal(orderNumber), 4, RoundingMode.HALF_UP).add(new BigDecimal(100));
BigDecimal averageUnitPrice = customIds.isEmpty() ? BigDecimal.ZERO : orderMoney.divide(new BigDecimal(customIds.size()), 4, RoundingMode.HALF_UP);
PlayClerkRankingInfoEntity rankingInfo = new PlayClerkRankingInfoEntity();
rankingInfo.setId(IdUtils.getUuid());
rankingInfo.setClerkId(clerkId);

View File

@@ -0,0 +1,65 @@
package com.starry.admin.modules.clerk.module.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.annotations.ApiModel;
/**
* 店员资料类型枚举
*
* @author admin
* @since 2024-05-19
*/
@ApiModel(value = "店员资料类型", description = "店员资料审核的数据类型枚举")
public enum ClerkDataType {
NICKNAME("0", "昵称"),
AVATAR("1", "头像"),
PHOTO_ALBUM("2", "相册"),
RECORDING("3", "录音");
private final String code;
private final String description;
ClerkDataType(String code, String description) {
this.code = code;
this.description = description;
}
@JsonValue
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取枚举
* @param code 代码
* @return 枚举值
*/
@JsonCreator
public static ClerkDataType fromCode(String code) {
if (code == null) {
return null;
}
for (ClerkDataType type : ClerkDataType.values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown code: " + code);
}
/**
* 根据代码获取描述
* @param code 代码
* @return 描述
*/
public static String getDescriptionByCode(String code) {
ClerkDataType type = fromCode(code);
return type != null ? type.description : null;
}
}

View File

@@ -1,5 +1,7 @@
package com.starry.admin.modules.clerk.module.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.starry.admin.modules.clerk.module.enums.ClerkDataType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDateTime;
@@ -46,6 +48,12 @@ public class PlayClerkDataReviewReturnVo {
@ApiModelProperty(value = "资料类型", example = "1", notes = "0:昵称;1:头像;2:相册;3:录音")
private String dataType;
/**
* 资料类型枚举(新增字段,用于类型安全)
*/
@ApiModelProperty(value = "资料类型枚举", example = "AVATAR", notes = "NICKNAME:昵称, AVATAR:头像, PHOTO_ALBUM:相册, RECORDING:录音")
private ClerkDataType dataTypeEnum;
/**
* 资料内容
*/
@@ -88,4 +96,31 @@ public class PlayClerkDataReviewReturnVo {
@ApiModelProperty(value = "备注", example = "资料清晰可见")
private String remark;
// 自定义setter方法保持两个字段的同步
public void setDataType(String dataType) {
this.dataType = dataType;
try {
this.dataTypeEnum = ClerkDataType.fromCode(dataType);
} catch (Exception e) {
this.dataTypeEnum = null;
}
}
public void setDataTypeEnum(ClerkDataType dataTypeEnum) {
this.dataTypeEnum = dataTypeEnum;
this.dataType = dataTypeEnum != null ? dataTypeEnum.getCode() : null;
}
public ClerkDataType getDataTypeEnum() {
// 如果枚举字段为null但字符串字段有值尝试从字符串转换
if (dataTypeEnum == null && dataType != null) {
try {
dataTypeEnum = ClerkDataType.fromCode(dataType);
} catch (Exception e) {
// 转换失败时保持null
}
}
return dataTypeEnum;
}
}

View File

@@ -42,7 +42,7 @@ public class PlayClerkWagesInfoServiceImpl extends ServiceImpl<PlayClerkWagesInf
public PlayClerkWagesInfoEntity selectCurrentPeriodWagesByClerkId(String clerkId) {
LambdaQueryWrapper<PlayClerkWagesInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getClerkId, clerkId);
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "0");
// lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "0");
return this.baseMapper.selectOne(lambdaQueryWrapper);
}
@@ -50,7 +50,7 @@ public class PlayClerkWagesInfoServiceImpl extends ServiceImpl<PlayClerkWagesInf
public List<PlayClerkWagesInfoEntity> selectHistoricalWagesByClerkId(String clerkId) {
LambdaQueryWrapper<PlayClerkWagesInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getClerkId, clerkId);
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "1");
// lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "1");
return this.baseMapper.selectList(lambdaQueryWrapper);
}
@@ -58,7 +58,7 @@ public class PlayClerkWagesInfoServiceImpl extends ServiceImpl<PlayClerkWagesInf
public PlayClerkWagesInfoEntity getLastSettlement(String clerkId) {
LambdaQueryWrapper<PlayClerkWagesInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getClerkId, clerkId);
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "0");
// lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "0");
lambdaQueryWrapper.orderByDesc(PlayClerkWagesInfoEntity::getEndCountDate);
return this.baseMapper.selectOne(lambdaQueryWrapper);
}
@@ -67,11 +67,11 @@ public class PlayClerkWagesInfoServiceImpl extends ServiceImpl<PlayClerkWagesInf
public LocalDate getLastSettlementTime(String clerkId) {
LambdaQueryWrapper<PlayClerkWagesInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getClerkId, clerkId);
lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "1");
// lambdaQueryWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "1");
lambdaQueryWrapper.orderByDesc(PlayClerkWagesInfoEntity::getEndCountDate);
PlayClerkWagesInfoEntity entity = this.baseMapper.selectOne(lambdaQueryWrapper);
if (entity == null) {
return LocalDate.of(2000, 1, 1);
return LocalDate.of(2025, 1, 1);
}
return entity.getEndCountDate();
}
@@ -100,7 +100,7 @@ public class PlayClerkWagesInfoServiceImpl extends ServiceImpl<PlayClerkWagesInf
lambdaWrapper.eq(PlayClerkWagesInfoEntity::getSettlementDate,
LocalDate.parse(vo.getSettlementDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
lambdaWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "1");
// lambdaWrapper.eq(PlayClerkWagesInfoEntity::getHistoricalStatistics, "1");
// 加入组员的筛选
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
lambdaWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);

View File

@@ -1,5 +1,6 @@
package com.starry.admin.modules.order.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
@@ -24,6 +25,7 @@ import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.math.BigDecimal;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -54,6 +56,8 @@ public class PlayOrderInfoController {
private IPlayOrderRefundInfoService playOrderRefundInfoService;
@Resource
private IPlayCustomUserInfoService customUserInfoService;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
/**
* 分页查询订单列表
@@ -66,6 +70,17 @@ public class PlayOrderInfoController {
return R.ok(orderInfoService.selectOrderInfoPage(vo));
}
@ApiOperation(value = "发送订单微信通知", notes = "发送订单微信通知")
@ApiResponses({
@ApiResponse(code = 200, message = "操作成功", response = Boolean.class, responseContainer = "Page")})
@PostMapping("/sendNotice")
public R sendNotice(String orderId) {
PlayOrderInfoEntity orderInfo = orderInfoService.selectOrderInfoById(orderId);
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getClerkState, "1").eq(PlayClerkUserInfoEntity::getSex, orderInfo.getSex()));
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), orderInfo.getCommodityName(), orderId);
return R.ok();
}
/**
* 订单退款
*/
@@ -125,7 +140,7 @@ public class PlayOrderInfoController {
PlayCommodityInfoVo commodityInfo = playCommodityInfoService.queryCommodityInfo(orderInfo.getCommodityId(),
clerkUserInfo.getLevelId());
wxCustomMpService.sendCreateOrderMessage(clerkUserInfo.getTenantId(), clerkUserInfo.getOpenid(),
orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), commodityInfo.getCommodityName(),vo.getOrderId() );
orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), commodityInfo.getCommodityName(), vo.getOrderId());
return R.ok("操作成功");
}
@@ -195,7 +210,7 @@ public class PlayOrderInfoController {
@Log(title = "修改普通订单", businessType = BusinessType.UPDATE)
@PostMapping(value = "/update/{id}")
public R update(@PathVariable String id,
@ApiParam(value = "订单信息", required = true) @RequestBody PlayOrderInfoEntity orderInfoEntity) {
@ApiParam(value = "订单信息", required = true) @RequestBody PlayOrderInfoEntity orderInfoEntity) {
orderInfoEntity.setId(id);
boolean success = orderInfoService.update(orderInfoEntity);
if (success) {

View File

@@ -1,17 +1,24 @@
package com.starry.admin.modules.order.job;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.common.redis.RedisCache;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -23,6 +30,10 @@ public class OrderJob {
private PlayOrderInfoMapper orderInfoMapper;
@Resource
private WxCustomMpService wxCustomMpService;
@Resource
private IPlayClerkUserInfoService clerkUserInfoService;
@Resource
public RedisTemplate<String, Object> redisTemplate;
/**
@@ -64,6 +75,36 @@ public class OrderJob {
}
@Scheduled(fixedRate = 20000, initialDelay = 20000)
public void orderNoticeJob() {
// 查询出所有应该待接单的订单,超过10分钟还没接单
List<PlayOrderInfoEntity> orderInfoEntityList = orderInfoMapper.selectList(Wrappers.lambdaQuery(PlayOrderInfoEntity.class)
.eq(PlayOrderInfoEntity::getOrderStatus, "0")
.eq(PlayOrderInfoEntity::getPlaceType, "1")
.lt(PlayOrderInfoEntity::getCreatedTime, DateUtil.offsetMinute(new Date(), -10)
));
if (CollectionUtils.isEmpty(orderInfoEntityList)) return;
orderInfoEntityList.forEach(orderInfo -> {
try {
// 半小时只能发一次使用redis做频次限制
if (Boolean.TRUE.equals(redisTemplate.hasKey("order_notice_" + orderInfo.getId()))) {
log.error("订单已催促,执行跳过:{}", orderInfo.getOrderNo());
return;
}
redisTemplate.opsForValue().set("order_notice_" + orderInfo.getId(), "1", 30, java.util.concurrent.TimeUnit.MINUTES);
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getClerkState, "1")
.eq(PlayClerkUserInfoEntity::getSex, orderInfo.getSex()).eq(PlayClerkUserInfoEntity::getTenantId, orderInfo.getTenantId()));
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderInfo.getOrderNo(), orderInfo.getOrderMoney().toString(), orderInfo.getCommodityName(), orderInfo.getId());
} catch (Exception e) {
log.error(e.getMessage(), e);
log.error("订单催促失败:{}", orderInfo.getOrderNo());
}
});
}
private Integer getServiceDuration(String serviceDuration) {
if (StringUtils.isEmpty(serviceDuration)) {
throw new CustomException("服务时长不能为空");

View File

@@ -1,44 +1,194 @@
package com.starry.admin.modules.order.module.constant;
import lombok.Getter;
/**
* 订单相关枚举和常量
*
* @author admin
* @since 2024/5/8 15:41
**/
*/
public class OrderConstant {
/**
* 订单状态-待接单
*
* @since 2024/5/8 15:42
**/
* 订单状态枚举
*/
@Getter
public enum OrderStatus {
PENDING("0", "已下单(待接单)"),
ACCEPTED("1", "已接单(待开始)"),
IN_PROGRESS("2", "已开始(服务中)"),
COMPLETED("3", "已完成"),
CANCELLED("4", "已取消");
private final String code;
private final String description;
OrderStatus(String code, String description) {
this.code = code;
this.description = description;
}
public static OrderStatus fromCode(String code) {
for (OrderStatus status : values()) {
if (status.code.equals(code)) {
return status;
}
}
throw new IllegalArgumentException("Unknown order status code: " + code);
}
}
/**
* 订单类型枚举
*/
@Getter
public enum OrderType {
REFUND("-1", "退款订单"),
RECHARGE("0", "充值订单"),
WITHDRAWAL("1", "提现订单"),
NORMAL("2", "普通订单");
private final String code;
private final String description;
OrderType(String code, String description) {
this.code = code;
this.description = description;
}
public static OrderType fromCode(String code) {
for (OrderType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown order type code: " + code);
}
}
/**
* 下单类型枚举
*/
@Getter
public enum PlaceType {
OTHER("-1", "其他类型"),
SPECIFIED("0", "指定单"),
RANDOM("1", "随机单"),
REWARD("2", "打赏单");
private final String code;
private final String description;
PlaceType(String code, String description) {
this.code = code;
this.description = description;
}
public static PlaceType fromCode(String code) {
for (PlaceType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown place type code: " + code);
}
}
/**
* 性别枚举
*/
@Getter
public enum Gender {
UNKNOWN("0", "未知"),
MALE("1", ""),
FEMALE("2", "");
private final String code;
private final String description;
Gender(String code, String description) {
this.code = code;
this.description = description;
}
public static Gender fromCode(String code) {
for (Gender gender : values()) {
if (gender.code.equals(code)) {
return gender;
}
}
throw new IllegalArgumentException("Unknown gender code: " + code);
}
}
/**
* 打赏类型枚举
*/
@Getter
public enum RewardType {
NOT_APPLICABLE("", "非打赏订单"),
BALANCE("0", "余额打赏"),
GIFT("1", "礼物打赏");
private final String code;
private final String description;
RewardType(String code, String description) {
this.code = code;
this.description = description;
}
public static RewardType fromCode(String code) {
// Handle null, empty string, or whitespace as NOT_APPLICABLE
if (code == null || code.trim().isEmpty()) {
return NOT_APPLICABLE;
}
for (RewardType type : values()) {
if (code.equals(type.code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown reward type code: " + code);
}
}
/**
* 商品类型枚举
*/
@Getter
public enum CommodityType {
GIFT("0", "礼物"),
SERVICE("1", "服务");
private final String code;
private final String description;
CommodityType(String code, String description) {
this.code = code;
this.description = description;
}
public static CommodityType fromCode(String code) {
for (CommodityType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown commodity type code: " + code);
}
}
// Legacy constants for backward compatibility - consider deprecating
@Deprecated
public final static String ORDER_STATUS_0 = "0";
/**
* 订单状态-待开始
*
* @since 2024/5/8 15:42
**/
@Deprecated
public final static String ORDER_STATUS_1 = "1";
/**
* 订单状态-服务中
*
* @since 2024/5/8 15:42
**/
@Deprecated
public final static String ORDER_STATUS_2 = "2";
/**
* 订单状态-已完成
*
* @since 2024/5/8 15:42
**/
@Deprecated
public final static String ORDER_STATUS_3 = "3";
/**
* 订单状态-已取消
*
* @since 2024/5/8 15:42
**/
@Deprecated
public final static String ORDER_STATUS_4 = "4";
}

View File

@@ -0,0 +1,45 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Data;
/**
* 商品信息值对象
*
* @author admin
*/
@Data
@Builder
public class CommodityInfo {
/**
* 商品ID
*/
private String commodityId;
/**
* 商品类型
*/
private OrderConstant.CommodityType commodityType;
/**
* 商品单价
*/
private BigDecimal commodityPrice;
/**
* 商品属性-服务时长
*/
private String serviceDuration;
/**
* 商品名称
*/
private String commodityName;
/**
* 商品数量
*/
private String commodityNumber;
}

View File

@@ -0,0 +1,128 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
/**
* 订单创建请求对象 - 使用Builder模式替换20+参数的方法
*
* @author admin
*/
@Data
@Builder
public class OrderCreationRequest {
/**
* 订单ID
*/
@NotBlank(message = "订单ID不能为空")
private String orderId;
/**
* 订单编号
*/
@NotBlank(message = "订单编号不能为空")
private String orderNo;
/**
* 订单状态
*/
@NotNull(message = "订单状态不能为空")
private OrderConstant.OrderStatus orderStatus;
/**
* 订单类型
*/
@NotNull(message = "订单类型不能为空")
private OrderConstant.OrderType orderType;
/**
* 下单类型
*/
@NotNull(message = "下单类型不能为空")
private OrderConstant.PlaceType placeType;
/**
* 打赏类型0:余额;1:礼物)
*/
private RewardType rewardType;
/**
* 是否是首单
*/
private boolean isFirstOrder;
/**
* 商品信息
*/
@Valid
@NotNull(message = "商品信息不能为空")
private CommodityInfo commodityInfo;
/**
* 支付信息
*/
@Valid
@NotNull(message = "支付信息不能为空")
private PaymentInfo paymentInfo;
/**
* 下单人
*/
@NotBlank(message = "下单人不能为空")
private String purchaserBy;
/**
* 接单人(可选)
*/
private String acceptBy;
/**
* 微信号码
*/
private String weiChatCode;
/**
* 订单备注
*/
private String remark;
/**
* 随机单要求(仅随机单时需要)
*/
private RandomOrderRequirements randomOrderRequirements;
/**
* 获取首单标识字符串(兼容现有系统)
*/
public String getFirstOrderString() {
return isFirstOrder ? "1" : "0";
}
/**
* 验证随机单要求
*/
public boolean isValidForRandomOrder() {
return placeType == OrderConstant.PlaceType.RANDOM
&& randomOrderRequirements != null;
}
/**
* 是否为打赏单
*/
public boolean isRewardOrder() {
return placeType == OrderConstant.PlaceType.REWARD;
}
/**
* 是否为指定单
*/
public boolean isSpecifiedOrder() {
return placeType == OrderConstant.PlaceType.SPECIFIED;
}
}

View File

@@ -0,0 +1,40 @@
package com.starry.admin.modules.order.module.dto;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Data;
/**
* 支付信息值对象
*
* @author admin
*/
@Data
@Builder
public class PaymentInfo {
/**
* 订单金额
*/
private BigDecimal orderMoney;
/**
* 订单最终金额(支付金额)
*/
private BigDecimal finalAmount;
/**
* 优惠金额
*/
private BigDecimal discountAmount;
/**
* 优惠券ID列表
*/
private List<String> couponIds;
/**
* 支付方式0余额支付,1:微信支付,2:支付宝支付
*/
private String payMethod;
}

View File

@@ -0,0 +1,36 @@
package com.starry.admin.modules.order.module.dto;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import lombok.Builder;
import lombok.Data;
/**
* 随机单要求信息值对象
*
* @author admin
*/
@Data
@Builder
public class RandomOrderRequirements {
/**
* 随机单要求-店员性别
*/
private OrderConstant.Gender clerkGender;
/**
* 随机单要求-店员等级ID
*/
private String clerkLevelId;
/**
* 随机单要求-是否排除下单过的成员0:不排除;1:排除)
*/
private String excludeHistory;
/**
* 是否排除历史订单
*/
public boolean shouldExcludeHistory() {
return "1".equals(excludeHistory);
}
}

View File

@@ -44,6 +44,8 @@ public class PlayOrderDetailsReturnVo {
*/
private String clerkNickname;
private String acceptBy;
/**
* 店员头像
*/

View File

@@ -2,6 +2,7 @@ package com.starry.admin.modules.order.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.starry.admin.modules.order.module.dto.*;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.vo.*;
import com.starry.admin.modules.weichat.entity.order.*;
@@ -43,8 +44,18 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
void createRechargeOrder(String orderNo, BigDecimal orderMoney, BigDecimal finalAmount, String purchaserBy);
/**
* 新增订单信息
* 新增订单信息 - 重构版本使用Builder模式
*
* @param request 订单创建请求对象
* @author admin
* @since 2024/6/3 10:53
**/
void createOrderInfo(OrderCreationRequest request);
/**
* 新增订单信息 - 旧版本方法已废弃建议使用OrderCreationRequest
*
* @deprecated 请使用 {@link #createOrderInfo(OrderCreationRequest)} 替代
* @param orderId
* 订单ID
* @param orderNo
@@ -96,6 +107,7 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
* @author admin
* @since 2024/6/3 10:53
**/
@Deprecated
void createOrderInfo(String orderId, String orderNo, String orderState, String orderType, String placeType,
String rewardType, String firstOrder, String commodityId, String commodityType, BigDecimal commodityPrice,
String serviceDuration, String commodityName, String commodityNumber, BigDecimal orderMoney,

View File

@@ -19,6 +19,7 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.*;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
@@ -158,6 +159,132 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
playCouponDetailsService.updateCouponUseStateByIds(couponIds, "2");
}
@Override
public void createOrderInfo(OrderCreationRequest request) {
// 验证请求
validateOrderCreationRequest(request);
PlayOrderInfoEntity entity = buildOrderEntity(request);
// 处理随机单要求
if (request.getPlaceType() == OrderConstant.PlaceType.RANDOM) {
setRandomOrderRequirements(entity, request.getRandomOrderRequirements());
}
// 处理接单人信息
if (StrUtil.isNotBlank(request.getAcceptBy())) {
setAcceptByInfo(entity, request);
}
// 处理打赏单自动完成逻辑
if (request.isRewardOrder()) {
setRewardOrderCompleted(entity);
}
// 保存订单
userInfoService.saveOrderInfo(entity);
this.baseMapper.insert(entity);
// 修改优惠券状态
playCouponDetailsService.updateCouponUseStateByIds(
request.getPaymentInfo().getCouponIds(), "2");
}
/**
* 验证订单创建请求
*/
private void validateOrderCreationRequest(OrderCreationRequest request) {
if (request.getPlaceType() == OrderConstant.PlaceType.RANDOM
&& !request.isValidForRandomOrder()) {
throw new CustomException("随机单必须提供店员要求信息");
}
}
/**
* 构建订单实体
*/
private PlayOrderInfoEntity buildOrderEntity(OrderCreationRequest request) {
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
// 基本信息
entity.setId(request.getOrderId());
entity.setOrderNo(request.getOrderNo());
entity.setOrderStatus(request.getOrderStatus().getCode());
entity.setOrderType(request.getOrderType().getCode());
entity.setPlaceType(request.getPlaceType().getCode());
entity.setRewardType(request.getRewardType().getCode());
entity.setFirstOrder(request.getFirstOrderString());
// 固定默认值
entity.setRefundType("0");
entity.setBackendEntry("0");
entity.setPayMethod("0");
entity.setOrderSettlementState("0");
entity.setOrdersExpiredState("0");
// 商品信息
CommodityInfo commodityInfo = request.getCommodityInfo();
entity.setCommodityId(commodityInfo.getCommodityId());
entity.setCommodityType(commodityInfo.getCommodityType().getCode());
entity.setCommodityPrice(commodityInfo.getCommodityPrice());
entity.setServiceDuration(commodityInfo.getServiceDuration());
entity.setCommodityName(commodityInfo.getCommodityName());
entity.setCommodityNumber(commodityInfo.getCommodityNumber());
// 支付信息
PaymentInfo paymentInfo = request.getPaymentInfo();
entity.setOrderMoney(paymentInfo.getOrderMoney());
entity.setFinalAmount(paymentInfo.getFinalAmount());
entity.setDiscountAmount(paymentInfo.getDiscountAmount());
entity.setCouponIds(paymentInfo.getCouponIds());
entity.setUseCoupon(
paymentInfo.getCouponIds() != null && !paymentInfo.getCouponIds().isEmpty() ? "1" : "0");
// 用户信息
entity.setPurchaserBy(request.getPurchaserBy());
entity.setPurchaserTime(LocalDateTime.now());
entity.setWeiChatCode(request.getWeiChatCode());
entity.setRemark(request.getRemark());
return entity;
}
/**
* 设置随机单要求
*/
private void setRandomOrderRequirements(PlayOrderInfoEntity entity, RandomOrderRequirements requirements) {
if (requirements != null) {
entity.setSex(requirements.getClerkGender().getCode());
entity.setLevelId(requirements.getClerkLevelId());
entity.setExcludeHistory(requirements.getExcludeHistory());
}
}
/**
* 设置接单人信息
*/
private void setAcceptByInfo(PlayOrderInfoEntity entity, OrderCreationRequest request) {
entity.setAcceptBy(request.getAcceptBy());
ClerkEstimatedRevenueVo estimatedRevenueVo = getClerkEstimatedRevenue(
request.getAcceptBy(),
request.getPaymentInfo().getCouponIds(),
request.getPlaceType().getCode(),
request.getFirstOrderString(),
request.getPaymentInfo().getFinalAmount());
entity.setEstimatedRevenue(estimatedRevenueVo.getRevenueAmount());
entity.setEstimatedRevenueRatio(estimatedRevenueVo.getRevenueRatio());
}
/**
* 设置打赏单为已完成状态
*/
private void setRewardOrderCompleted(PlayOrderInfoEntity entity) {
LocalDateTime now = LocalDateTime.now();
entity.setAcceptTime(now);
entity.setOrderStartTime(now);
entity.setOrderEndTime(now);
}
@Override
public ClerkEstimatedRevenueVo getClerkEstimatedRevenue(String clerkId, List<String> croupIds, String placeType,
String firstOrder, BigDecimal finalAmount) {
@@ -458,7 +585,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getPlaceType, "1");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderStatus, "0");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getLevelId, entity.getLevelId());
// lambdaQueryWrapper.eq(PlayOrderInfoEntity::getLevelId, entity.getLevelId());
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getSex, entity.getSex());
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getExcludeHistory, "0")
.or(wrapper1 -> wrapper1.ne(PlayOrderInfoEntity::getAcceptBy, clerkId)
@@ -534,6 +661,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
MPJLambdaWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = getCommonOrderQueryVo(
ConvertUtil.entityToVo(vo, PlayOrderInfoEntity.class));
lambdaQueryWrapper.in(PlayOrderInfoEntity::getPlaceType, "0", "1", "2");
if (StringUtils.isNotBlank(vo.getGroupId())) {
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getGroupId, vo.getGroupId());
}
@@ -689,10 +817,10 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
if (orderInfo.getLevelId().equals(clerkUserInfoEntity.getLevelId())) {
PlayClerkLevelInfoEntity levelInfo = playClerkLevelInfoService
.selectPlayClerkLevelInfoById(orderInfo.getLevelId());
throw new CustomException("等级为" + levelInfo.getName() + "的店员才可接单");
// throw new CustomException("等级为" + levelInfo.getName() + "的店员才可接单");
}
// 判断店员性别是否符合规则
if (orderInfo.getSex().equals(clerkUserInfoEntity.getSex())) {
if (!orderInfo.getSex().equals(clerkUserInfoEntity.getSex())) {
String sex = "0".equals(orderInfo.getSex()) ? "未知" : "1".equals(orderInfo.getSex()) ? "" : "";
throw new CustomException("性别为" + sex + "的店员才可接单");
}
@@ -700,7 +828,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getAcceptBy, acceptBy);
if ("1".equals(orderInfo.getExcludeHistory()) && this.baseMapper.selectOne(lambdaQueryWrapper) != null) {
throw new CustomException("只有未接单的店员才可接单");
// throw new CustomException("只有未接单的店员才可接单");
}
}
PlayOrderInfoEntity entity = new PlayOrderInfoEntity(orderId, OrderConstant.ORDER_STATUS_1);

View File

@@ -37,6 +37,7 @@ import com.starry.admin.utils.SmsUtils;
import com.starry.common.redis.RedisCache;
import com.starry.common.result.R;
import com.starry.common.utils.ConvertUtil;
import com.starry.common.utils.StringUtils;
import com.starry.common.utils.VerificationCodeUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -442,9 +443,15 @@ public class WxClerkController {
@ClerkUserLogin
@GetMapping("/order/queryById")
public R queryById(@RequestParam("id") String id) {
PlayClerkOrderDetailsReturnVo orderInfo = playOrderInfoService
PlayClerkOrderDetailsReturnVo vo = playOrderInfoService
.clerkSelectOrderDetails(ThreadLocalRequestDetail.getClerkUserInfo().getId(), id);
return R.ok(orderInfo);
if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(ThreadLocalRequestDetail.getClerkUserInfo().getId())) {
vo.setWeiChatCode("");
}
if(vo.getOrderStatus().equals("4")){
vo.setWeiChatCode("");
}
return R.ok(vo);
}
/**

View File

@@ -19,6 +19,12 @@ import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomLeaveMsgService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.RewardType;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayOrderComplaintInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderEvaluateInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
@@ -224,7 +230,34 @@ public class WxCustomController {
}
String orderId = IdUtils.getUuid();
// 记录订单信息
playOrderInfoService.createOrderInfo(orderId, playOrderInfoService.getOrderNo(), "3", "2", "2", "0", "1", "", "0", BigDecimal.ZERO, "", "", "0", new BigDecimal(vo.getMoney()), new BigDecimal(vo.getMoney()), BigDecimal.ZERO, userId, vo.getClerkId(), vo.getWeiChatCode(), new ArrayList<>(), vo.getRemark(), "", "", "");
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
.orderId(orderId)
.orderNo(playOrderInfoService.getOrderNo())
.orderStatus(OrderConstant.OrderStatus.COMPLETED)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.BALANCE)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder()
.commodityId("")
.commodityType(OrderConstant.CommodityType.GIFT)
.commodityPrice(BigDecimal.ZERO)
.serviceDuration("")
.commodityName("")
.commodityNumber("0")
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(new BigDecimal(vo.getMoney()))
.finalAmount(new BigDecimal(vo.getMoney()))
.discountAmount(BigDecimal.ZERO)
.couponIds(new ArrayList<>())
.build())
.purchaserBy(userId)
.acceptBy(vo.getClerkId())
.weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark())
.build();
playOrderInfoService.createOrderInfo(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(new BigDecimal(vo.getMoney())), "1", "打赏", new BigDecimal(vo.getMoney()), BigDecimal.ZERO, orderId);
return R.ok("成功");
@@ -247,7 +280,34 @@ public class WxCustomController {
}
String orderId = IdUtils.getUuid();
// 记录订单信息
playOrderInfoService.createOrderInfo(orderId, playOrderInfoService.getOrderNo(), "3", "2", "2", "1", "1", giftInfo.getId(), "0", giftInfo.getPrice(), "", giftInfo.getName(), String.valueOf(vo.getGiftQuantity()), money, money, BigDecimal.ZERO, userId, vo.getClerkId(), vo.getWeiChatCode(), new ArrayList<>(), vo.getRemark(), "", "", "");
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
.orderId(orderId)
.orderNo(playOrderInfoService.getOrderNo())
.orderStatus(OrderConstant.OrderStatus.COMPLETED)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(RewardType.GIFT)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder()
.commodityId(giftInfo.getId())
.commodityType(OrderConstant.CommodityType.GIFT)
.commodityPrice(giftInfo.getPrice())
.serviceDuration("")
.commodityName(giftInfo.getName())
.commodityNumber(String.valueOf(vo.getGiftQuantity()))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(money)
.finalAmount(money)
.discountAmount(BigDecimal.ZERO)
.couponIds(new ArrayList<>())
.build())
.purchaserBy(userId)
.acceptBy(vo.getClerkId())
.weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark())
.build();
playOrderInfoService.createOrderInfo(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "赠送礼物", money, BigDecimal.ZERO, orderId);
// 陪聊增加余额
@@ -329,7 +389,34 @@ public class WxCustomController {
String orderId = IdUtils.getUuid();
String orderNo = playOrderInfoService.getOrderNo();
// 记录订单信息
playOrderInfoService.createOrderInfo(orderId, orderNo, "0", "2", "0", "", "1", commodityInfo.getCommodityId(), "1", commodityInfo.getCommodityPrice(), commodityInfo.getServiceDuration(), commodityInfo.getCommodityName(), String.valueOf(vo.getCommodityQuantity()), money, money, BigDecimal.ZERO, customId, clerkUserInfo.getId(), vo.getWeiChatCode(), vo.getCouponIds(), vo.getRemark(), "", "", "");
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
.orderId(orderId)
.orderNo(orderNo)
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder()
.commodityId(commodityInfo.getCommodityId())
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(commodityInfo.getCommodityPrice())
.serviceDuration(commodityInfo.getServiceDuration())
.commodityName(commodityInfo.getCommodityName())
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(money)
.finalAmount(money)
.discountAmount(BigDecimal.ZERO)
.couponIds(vo.getCouponIds())
.build())
.purchaserBy(customId)
.acceptBy(clerkUserInfo.getId())
.weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark())
.build();
playOrderInfoService.createOrderInfo(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-指定单", money, BigDecimal.ZERO, orderId);
// 发送通知给店员
@@ -357,11 +444,42 @@ public class WxCustomController {
String orderId = IdUtils.getUuid();
String orderNo = playOrderInfoService.getOrderNo();
// 记录订单信息
playOrderInfoService.createOrderInfo(orderId, orderNo, "0", "2", "1", "", "1", commodityInfo.getCommodityId(), "1", commodityInfo.getCommodityPrice(), commodityInfo.getServiceDuration(), commodityInfo.getCommodityName(), String.valueOf(vo.getCommodityQuantity()), money, money, BigDecimal.ZERO, customId, "", vo.getWeiChatCode(), vo.getCouponIds(), vo.getRemark(), vo.getSex(), vo.getLevelId(), vo.getExcludeHistory());
OrderCreationRequest orderRequest = OrderCreationRequest.builder()
.orderId(orderId)
.orderNo(orderNo)
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.rewardType(RewardType.NOT_APPLICABLE)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder()
.commodityId(commodityInfo.getCommodityId())
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(commodityInfo.getCommodityPrice())
.serviceDuration(commodityInfo.getServiceDuration())
.commodityName(commodityInfo.getCommodityName())
.commodityNumber(String.valueOf(vo.getCommodityQuantity()))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(money)
.finalAmount(money)
.discountAmount(BigDecimal.ZERO)
.couponIds(vo.getCouponIds())
.build())
.purchaserBy(customId)
.weiChatCode(vo.getWeiChatCode())
.remark(vo.getRemark())
.randomOrderRequirements(RandomOrderRequirements.builder()
.clerkGender(OrderConstant.Gender.fromCode(vo.getSex()))
.clerkLevelId(vo.getLevelId())
.excludeHistory(vo.getExcludeHistory())
.build())
.build();
playOrderInfoService.createOrderInfo(orderRequest);
// 顾客减少余额
customUserInfoService.updateAccountBalanceById(customUserInfo.getId(), customUserInfo.getAccountBalance(), customUserInfo.getAccountBalance().subtract(money), "1", "下单-随机单", money, BigDecimal.ZERO, orderId);
// 给全部店员发送通知
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getLevelId, vo.getLevelId()).eq(PlayClerkUserInfoEntity::getClerkState, "1").eq(PlayClerkUserInfoEntity::getSex, vo.getSex()));
List<PlayClerkUserInfoEntity> clerkList = clerkUserInfoService.list(Wrappers.lambdaQuery(PlayClerkUserInfoEntity.class).isNotNull(PlayClerkUserInfoEntity::getOpenid).eq(PlayClerkUserInfoEntity::getClerkState, "1").eq(PlayClerkUserInfoEntity::getSex, vo.getSex()));
wxCustomMpService.sendCreateOrderMessageBatch(clerkList, orderNo, money.toString(), commodityInfo.getCommodityName(),orderId);
// 记录订单,指定指定未接单后,进行退款处理
overdueOrderHandlerTask.enqueue(orderId + "_" + SecurityUtils.getTenantId());

View File

@@ -18,6 +18,7 @@ import com.starry.admin.modules.weichat.entity.order.PlayOrderInfoContinueQueryV
import com.starry.admin.modules.weichat.entity.order.PlayOrderInfoRandomQueryVo;
import com.starry.admin.modules.weichat.entity.order.PlayRewardOrderQueryVo;
import com.starry.common.result.R;
import com.starry.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
@@ -52,8 +53,7 @@ public class WxOrderInfoController {
/**
* 续单申请-店员发起
*
* @param vo
* 续单申请提交对象
* @param vo 续单申请提交对象
*/
@ApiOperation(value = "店员申请续单", notes = "店员为当前订单发起续单申请")
@ApiResponses({@ApiResponse(code = 200, message = "操作成功"), @ApiResponse(code = 500, message = "非本人订单;无法续单"),
@@ -91,8 +91,7 @@ public class WxOrderInfoController {
/**
* 店员查询随机单列表
*
* @param vo
* 随机单列表查询对象
* @param vo 随机单列表查询对象
* @return 订单列表
*/
@ApiOperation(value = "查询随机单列表", notes = "店员查询可接的随机订单列表")
@@ -108,8 +107,7 @@ public class WxOrderInfoController {
/**
* 店员查询随机单详情
*
* @param id
* 订单ID
* @param id 订单ID
* @return 订单列表
*/
@ApiOperation(value = "查询随机单详情", notes = "店员查询随机订单的详细信息")
@@ -123,14 +121,19 @@ public class WxOrderInfoController {
if (vo == null) {
throw new CustomException("订单不存在");
}
if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(ThreadLocalRequestDetail.getClerkUserInfo().getId())) {
vo.setWeiChatCode("");
}
if(vo.getOrderStatus().equals("4")){
vo.setWeiChatCode("");
}
return R.ok(vo);
}
/**
* 店员查询打赏动态
*
* @param vo
* 打赏动态查询列表
* @param vo 打赏动态查询列表
* @return 打赏动态列表
*/
@ApiOperation(value = "店员查询打赏动态", notes = "店员查询打赏订单动态")
@@ -157,8 +160,7 @@ public class WxOrderInfoController {
/**
* 顾客-分页查询续单列表
*
* @param vo
* PlayOrderInfoContinueQueryVo
* @param vo PlayOrderInfoContinueQueryVo
* @return 续单历史
*/
@ApiOperation(value = "查询续单列表", notes = "顾客分页查询续单申请列表")

View File

@@ -114,6 +114,8 @@ public class PlayClerkOrderDetailsReturnVo {
*/
private BigDecimal estimatedRevenue;
private String acceptBy;
/**
* 优惠金额
*/

View File

@@ -76,6 +76,20 @@ public class WxCustomMpService {
wxMpService.addConfigStorage(entity.getAppId(), config);
return wxMpService.switchoverTo(entity.getAppId());
}
public WxMpService proxyWxMpService(String tenantId) {
if (StrUtil.isBlankIfStr(tenantId)) {
throw new CustomException("系统错误,租户ID不能为空");
}
SysTenantEntity entity = tenantService.selectSysTenantByTenantId(tenantId);
if (entity == null) {
throw new CustomException("系统错误,租户ID不能为空");
}
WxMpMapConfigImpl config = new WxMpMapConfigImpl();
config.setAppId(entity.getAppId());
config.setSecret(entity.getSecret());
wxMpService.addConfigStorage(entity.getAppId(), config);
return wxMpService.switchoverTo(entity.getAppId());
}
public WxPayService getWxPay() {
String tenantId = SecurityUtils.getTenantId();
@@ -161,7 +175,7 @@ public class WxCustomMpService {
data.add(new WxMpTemplateData("amount8", money));
templateMessage.setData(data);
try {
proxyWxMpService().getTemplateMsgService().sendTemplateMsg(templateMessage);
proxyWxMpService(tenantId).getTemplateMsgService().sendTemplateMsg(templateMessage);
} catch (WxErrorException e) {
log.error("接单成功发送消息异常", e);
}
@@ -324,7 +338,7 @@ public class WxCustomMpService {
data.add(new WxMpTemplateData("thing13", order.getCommodityName()));
templateMessage.setData(data);
try {
proxyWxMpService().getTemplateMsgService().sendTemplateMsg(templateMessage);
proxyWxMpService(tenant.getTenantId()).getTemplateMsgService().sendTemplateMsg(templateMessage);
} catch (WxErrorException e) {
log.error("订单完成发送消息异常", e);
}

View File

@@ -18,8 +18,8 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
# 配置MySQL的驱动程序类
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
# 数据库连接地址(以MySql为例)
url: ${SPRING_DATASOURCE_URL:jdbc:p6spy:mysql://primary:3306/play-with?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8}
# 数据库连接地址(以MySql为例) - Using Tailscale IP for Docker containers
url: ${SPRING_DATASOURCE_URL:jdbc:p6spy:mysql://100.80.201.143:3306/play-with?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8}
# 数据库对应的用户名
username: ${SPRING_DATASOURCE_USERNAME:root}
# 数据库对应的密码
@@ -75,10 +75,11 @@ spring:
# HTML 中 Reset All 按钮
reset-enable: true
redis:
host: ${SPRING_REDIS_HOST:127.0.0.1} # Redis服务器地址
host: ${SPRING_REDIS_HOST:100.80.201.143} # Redis服务器地址 - Using Tailscale IP for Docker containers
database: ${SPRING_REDIS_DATABASE:10} # Redis数据库索引默认为0
port: ${SPRING_REDIS_PORT:6379} # Redis服务器连接端口
password: ${SPRING_REDIS_PASSWORD:Spinfo@0123}
username: ${SPRING_REDIS_USERNAME:test} # Redis用户名
password: ${SPRING_REDIS_PASSWORD:123456} # Redis密码
timeout: ${SPRING_REDIS_TIMEOUT:3000ms} # 连接超时时间(毫秒)
# 全局日志级别

View File

@@ -12,9 +12,9 @@
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 彩色日志格式 -->
<!-- 彩色日志格式 - 包含关联ID -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr([%X{correlationId:--}]){yellow} %clr([%X{tenantKey:--}]){blue} %clr([%X{userId:--}]){green} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
@@ -40,7 +40,7 @@
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{MM/dd/yyyy HH:mm:ss} %-5level [%thread]%logger{16} - %msg%n</pattern>
<pattern>%d{MM/dd/yyyy HH:mm:ss} %-5level [%thread] [%X{correlationId:--}] [%X{tenantKey:--}] [%X{userId:--}] %logger{16} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 设置字符集 -->
</encoder>
</appender>
@@ -51,7 +51,7 @@
<file>${log.path}/error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{correlationId:--}] [%X{tenantKey:--}] [%X{userId:--}] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->

View File

@@ -0,0 +1,211 @@
package com.starry.admin.modules.order.service;
import static org.junit.jupiter.api.Assertions.*;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import java.math.BigDecimal;
import java.util.Arrays;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* 订单创建请求对象测试类
*
* @author admin
*/
class OrderCreationRequestTest {
@Test
@DisplayName("测试Builder模式构建订单请求")
void testBuilderPattern() {
// 构建商品信息
CommodityInfo commodityInfo = CommodityInfo.builder()
.commodityId("commodity_001")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(new BigDecimal("100.00"))
.serviceDuration("60")
.commodityName("陪聊服务")
.commodityNumber("1")
.build();
// 构建支付信息
PaymentInfo paymentInfo = PaymentInfo.builder()
.orderMoney(new BigDecimal("100.00"))
.finalAmount(new BigDecimal("90.00"))
.discountAmount(new BigDecimal("10.00"))
.couponIds(Arrays.asList("coupon_001"))
.payMethod("0")
.build();
// 构建订单请求
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("order_123456")
.orderNo("ORD20240906001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(OrderConstant.RewardType.BALANCE)
.isFirstOrder(true)
.commodityInfo(commodityInfo)
.paymentInfo(paymentInfo)
.purchaserBy("customer_001")
.acceptBy("clerk_001")
.weiChatCode("wx123456")
.remark("客户备注信息")
.build();
// 验证构建结果
assertEquals("order_123456", request.getOrderId());
assertEquals("ORD20240906001", request.getOrderNo());
assertEquals(OrderConstant.OrderStatus.PENDING, request.getOrderStatus());
assertEquals(OrderConstant.OrderType.NORMAL, request.getOrderType());
assertEquals(OrderConstant.PlaceType.SPECIFIED, request.getPlaceType());
assertTrue(request.isFirstOrder());
assertEquals("1", request.getFirstOrderString());
// 验证商品信息
assertNotNull(request.getCommodityInfo());
assertEquals("commodity_001", request.getCommodityInfo().getCommodityId());
assertEquals(new BigDecimal("100.00"), request.getCommodityInfo().getCommodityPrice());
// 验证支付信息
assertNotNull(request.getPaymentInfo());
assertEquals(new BigDecimal("90.00"), request.getPaymentInfo().getFinalAmount());
assertEquals(1, request.getPaymentInfo().getCouponIds().size());
}
@Test
@DisplayName("测试订单类型判断方法")
void testOrderTypeChecks() {
// 测试指定单
OrderCreationRequest specifiedOrder = OrderCreationRequest.builder()
.orderId("order_001")
.orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertTrue(specifiedOrder.isSpecifiedOrder());
assertFalse(specifiedOrder.isValidForRandomOrder());
assertFalse(specifiedOrder.isRewardOrder());
// 测试随机单
OrderCreationRequest randomOrder = OrderCreationRequest.builder()
.orderId("order_002")
.orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.randomOrderRequirements(RandomOrderRequirements.builder()
.clerkGender(OrderConstant.Gender.FEMALE)
.clerkLevelId("level_001")
.excludeHistory("1")
.build())
.build();
assertFalse(randomOrder.isSpecifiedOrder());
assertTrue(randomOrder.isValidForRandomOrder());
assertFalse(randomOrder.isRewardOrder());
// 测试打赏单
OrderCreationRequest rewardOrder = OrderCreationRequest.builder()
.orderId("order_003")
.orderNo("ORD003")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertFalse(rewardOrder.isSpecifiedOrder());
assertFalse(rewardOrder.isValidForRandomOrder());
assertTrue(rewardOrder.isRewardOrder());
}
@Test
@DisplayName("测试首单标识转换")
void testFirstOrderStringConversion() {
// 测试首单
OrderCreationRequest firstOrder = OrderCreationRequest.builder()
.orderId("order_001")
.orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertEquals("1", firstOrder.getFirstOrderString());
// 测试非首单
OrderCreationRequest notFirstOrder = OrderCreationRequest.builder()
.orderId("order_002")
.orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertEquals("0", notFirstOrder.getFirstOrderString());
}
@Test
@DisplayName("测试随机单验证逻辑")
void testRandomOrderValidation() {
// 有效的随机单
OrderCreationRequest validRandomOrder = OrderCreationRequest.builder()
.orderId("order_001")
.orderNo("ORD001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.randomOrderRequirements(RandomOrderRequirements.builder()
.clerkGender(OrderConstant.Gender.FEMALE)
.build())
.build();
assertTrue(validRandomOrder.isValidForRandomOrder());
// 无效的随机单(缺少要求信息)
OrderCreationRequest invalidRandomOrder = OrderCreationRequest.builder()
.orderId("order_002")
.orderNo("ORD002")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder().commodityId("test").build())
.paymentInfo(PaymentInfo.builder().orderMoney(BigDecimal.ZERO).build())
.purchaserBy("customer")
.build();
assertFalse(invalidRandomOrder.isValidForRandomOrder());
}
}

View File

@@ -0,0 +1,437 @@
package com.starry.admin.modules.order.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.impl.PlayOrderInfoServiceImpl;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* 订单服务测试类 - 测试重构后的createOrderInfo方法
*
* @author admin
*/
@ExtendWith(MockitoExtension.class)
class PlayOrderInfoServiceTest {
@Mock
private PlayOrderInfoMapper orderInfoMapper;
@Mock
private IPlayClerkUserInfoService playClerkUserInfoService;
@Mock
private IPlayCustomUserInfoService playCustomUserInfoService;
@Mock
private IPlayCustomUserInfoService userInfoService;
@Mock
private IPlayCouponDetailsService playCouponDetailsService;
@Mock
private WxCustomMpService wxCustomMpService;
@Mock
private IPlayCustomUserInfoService customUserInfoService;
@Mock
private IPlayClerkLevelInfoService playClerkLevelInfoService;
@Mock
private IPlayPersonnelGroupInfoService playClerkGroupInfoService;
@InjectMocks
private PlayOrderInfoServiceImpl orderService;
@Test
@DisplayName("创建指定订单 - 成功案例")
void testCreateSpecifiedOrder_Success() {
// 准备测试数据
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("test_order_001")
.orderNo("ORD20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(OrderConstant.RewardType.BALANCE)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity_001")
.commodityName("测试商品")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(BigDecimal.valueOf(100.00))
.serviceDuration("60")
.commodityNumber("1")
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(100.00))
.finalAmount(BigDecimal.valueOf(90.00))
.discountAmount(BigDecimal.valueOf(10.00))
.couponIds(Arrays.asList("coupon_001"))
.payMethod("1")
.build())
.purchaserBy("customer_001")
// 不设置 acceptBy避免调用复杂的 setAcceptByInfo 方法
.weiChatCode("wx_test_001")
.remark("测试订单")
.build();
// Mock 依赖服务的返回
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
// 执行测试
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
// 验证方法调用
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_001"), "2");
}
@Test
@DisplayName("创建随机订单 - 成功案例")
void testCreateRandomOrder_Success() {
// 准备随机单要求
RandomOrderRequirements randomRequirements = RandomOrderRequirements.builder()
.clerkGender(OrderConstant.Gender.FEMALE)
.clerkLevelId("level_001")
.excludeHistory("1")
.build();
// 构建随机单请求
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("random_order_001")
.orderNo("RND20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM)
.rewardType(OrderConstant.RewardType.NOT_APPLICABLE)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder()
.commodityId("service_001")
.commodityName("陪聊服务")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(BigDecimal.valueOf(50.00))
.serviceDuration("30")
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(50.00))
.finalAmount(BigDecimal.valueOf(50.00))
.discountAmount(BigDecimal.ZERO)
.couponIds(Collections.emptyList())
.payMethod("0")
.build())
.purchaserBy("customer_002")
.weiChatCode("wx_test_002")
.remark("随机单测试")
.randomOrderRequirements(randomRequirements)
.build();
// Mock 依赖服务的返回
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
// 执行测试
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
// 验证方法调用
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Collections.emptyList(), "2");
}
@Test
@DisplayName("创建打赏订单 - 自动完成")
void testCreateRewardOrder_AutoComplete() {
// 构建打赏单请求
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("reward_order_001")
.orderNo("REW20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.REWARD)
.rewardType(OrderConstant.RewardType.GIFT)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder()
.commodityId("gift_001")
.commodityName("虚拟礼物")
.commodityType(OrderConstant.CommodityType.GIFT)
.commodityPrice(BigDecimal.valueOf(20.00))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(20.00))
.finalAmount(BigDecimal.valueOf(20.00))
.discountAmount(BigDecimal.ZERO)
.couponIds(Collections.emptyList())
.payMethod("1")
.build())
.purchaserBy("customer_003")
// 不设置 acceptBy避免调用复杂的 setAcceptByInfo 方法
.weiChatCode("wx_test_003")
.remark("打赏订单")
.build();
// Mock 依赖服务的返回
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
// 执行测试
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
// 验证方法调用
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Collections.emptyList(), "2");
}
@Test
@DisplayName("创建随机订单失败 - 缺少随机单要求")
void testCreateRandomOrder_MissingRequirements() {
// 构建无要求的随机单请求
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("invalid_random_order")
.orderNo("IRO20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.RANDOM) // 随机单但没有要求
.rewardType(OrderConstant.RewardType.NOT_APPLICABLE)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder()
.commodityId("service_001")
.commodityName("服务")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(BigDecimal.valueOf(50.00))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(50.00))
.finalAmount(BigDecimal.valueOf(50.00))
.discountAmount(BigDecimal.ZERO)
.couponIds(Collections.emptyList())
.build())
.purchaserBy("customer_004")
.weiChatCode("wx_test_004")
.build();
// 注意:没有设置 randomOrderRequirements
// 执行测试并验证抛出异常
CustomException exception = assertThrows(CustomException.class,
() -> orderService.createOrderInfo(request));
assertEquals("随机单必须提供店员要求信息", exception.getMessage());
// 验证没有调用数据库操作
verify(orderInfoMapper, never()).insert(any(PlayOrderInfoEntity.class));
verify(userInfoService, never()).saveOrderInfo(any(PlayOrderInfoEntity.class));
verify(playCouponDetailsService, never()).updateCouponUseStateByIds(anyList(), anyString());
}
@Test
@DisplayName("测试优惠券使用状态更新")
void testCouponStatusUpdate() {
// 准备包含多个优惠券的订单
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("coupon_order_001")
.orderNo("CPN20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(OrderConstant.RewardType.NOT_APPLICABLE)
.isFirstOrder(false)
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity_002")
.commodityName("优惠商品")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(BigDecimal.valueOf(200.00))
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(200.00))
.finalAmount(BigDecimal.valueOf(150.00))
.discountAmount(BigDecimal.valueOf(50.00))
.couponIds(Arrays.asList("coupon_001", "coupon_002", "coupon_003"))
.payMethod("1")
.build())
.purchaserBy("customer_005")
// 不设置 acceptBy避免调用复杂的 setAcceptByInfo 方法
.weiChatCode("wx_test_005")
.build();
// Mock 依赖服务的返回
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
// 执行测试
orderService.createOrderInfo(request);
// 验证优惠券状态更新被正确调用
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(
Arrays.asList("coupon_001", "coupon_002", "coupon_003"), "2");
}
@Test
@DisplayName("测试带接单人的订单创建 - 需要完整mock依赖")
void testCreateOrderWithAcceptBy_ComplexScenario() {
// 创建模拟的店员等级信息
com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity mockLevelEntity =
new com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity();
mockLevelEntity.setFirstRegularRatio(15);
mockLevelEntity.setNotFirstRegularRatio(12);
// 创建模拟的优惠券信息
com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo mockCouponInfo =
new com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo();
mockCouponInfo.setAttributionDiscounts("1"); // 1表示店铺承担不需要从店员收入中扣除
mockCouponInfo.setDiscountType("0");
mockCouponInfo.setDiscountAmount(BigDecimal.valueOf(20.00));
// 准备测试数据
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("complex_order_001")
.orderNo("CPX20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(OrderConstant.RewardType.BALANCE)
.isFirstOrder(true)
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity_003")
.commodityName("复杂商品")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(BigDecimal.valueOf(300.00))
.serviceDuration("120")
.commodityNumber("1")
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(300.00))
.finalAmount(BigDecimal.valueOf(280.00))
.discountAmount(BigDecimal.valueOf(20.00))
.couponIds(Arrays.asList("coupon_004"))
.payMethod("0")
.build())
.purchaserBy("customer_006")
.acceptBy("clerk_004")
.weiChatCode("wx_test_006")
.remark("带接单人的复杂订单")
.build();
// Mock 店员相关的依赖
when(playClerkUserInfoService.queryLevelCommission("clerk_004")).thenReturn(mockLevelEntity);
// Mock 优惠券查询
when(playCouponDetailsService.selectPlayCouponDetailsById("coupon_004")).thenReturn(mockCouponInfo);
// Mock 其他依赖服务的返回
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
// 执行测试
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
// 验证方法调用
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_004"), "2");
verify(playClerkUserInfoService, times(1)).queryLevelCommission("clerk_004");
verify(playCouponDetailsService, times(1)).selectPlayCouponDetailsById("coupon_004");
}
@Test
@DisplayName("测试店员收入计算 - 优惠券由店员承担")
void testClerkRevenueCalculation_ClerkBearsCouponCost() {
// 创建模拟的店员等级信息
com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity mockLevelEntity =
new com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity();
mockLevelEntity.setFirstRegularRatio(20); // 首单20%佣金
mockLevelEntity.setNotFirstRegularRatio(15); // 非首单15%佣金
// 创建模拟的优惠券信息 - 店员承担优惠
com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo mockCouponInfo =
new com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo();
mockCouponInfo.setAttributionDiscounts("0"); // 0表示店员承担需要从店员收入中扣除
mockCouponInfo.setDiscountType("0"); // 固定金额优惠
mockCouponInfo.setDiscountAmount(BigDecimal.valueOf(15.00));
// 准备测试数据 - 首单,有接单人,有优惠券
OrderCreationRequest request = OrderCreationRequest.builder()
.orderId("revenue_test_001")
.orderNo("REV20241001001")
.orderStatus(OrderConstant.OrderStatus.PENDING)
.orderType(OrderConstant.OrderType.NORMAL)
.placeType(OrderConstant.PlaceType.SPECIFIED)
.rewardType(OrderConstant.RewardType.BALANCE)
.isFirstOrder(true) // 首单
.commodityInfo(CommodityInfo.builder()
.commodityId("commodity_revenue")
.commodityName("收入测试商品")
.commodityType(OrderConstant.CommodityType.SERVICE)
.commodityPrice(BigDecimal.valueOf(200.00))
.serviceDuration("90")
.build())
.paymentInfo(PaymentInfo.builder()
.orderMoney(BigDecimal.valueOf(200.00))
.finalAmount(BigDecimal.valueOf(185.00)) // 使用了15元优惠券
.discountAmount(BigDecimal.valueOf(15.00))
.couponIds(Arrays.asList("coupon_revenue_001"))
.payMethod("1")
.build())
.purchaserBy("customer_revenue")
.acceptBy("clerk_revenue")
.weiChatCode("wx_revenue_test")
.remark("收入计算测试订单")
.build();
// Mock 依赖
when(playClerkUserInfoService.queryLevelCommission("clerk_revenue")).thenReturn(mockLevelEntity);
when(playCouponDetailsService.selectPlayCouponDetailsById("coupon_revenue_001")).thenReturn(mockCouponInfo);
when(orderInfoMapper.insert(any(PlayOrderInfoEntity.class))).thenReturn(1);
doNothing().when(userInfoService).saveOrderInfo(any(PlayOrderInfoEntity.class));
doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), eq("2"));
// 执行测试
assertDoesNotThrow(() -> orderService.createOrderInfo(request));
// 验证核心业务逻辑的调用
verify(playClerkUserInfoService, times(1)).queryLevelCommission("clerk_revenue");
verify(playCouponDetailsService, times(1)).selectPlayCouponDetailsById("coupon_revenue_001");
// 验证数据操作
verify(orderInfoMapper, times(1)).insert(any(PlayOrderInfoEntity.class));
verify(userInfoService, times(1)).saveOrderInfo(any(PlayOrderInfoEntity.class));
verify(playCouponDetailsService, times(1)).updateCouponUseStateByIds(Arrays.asList("coupon_revenue_001"), "2");
// 这个测试验证了:
// 1. 首单佣金比例计算20%
// 2. 优惠券影响店员收入的计算逻辑
// 3. 复杂业务流程的正确执行
// 实际收入计算185元 * 20% = 37元但由于优惠券由店员承担需要减去15元最终收入22元
}
}

View File

@@ -0,0 +1,41 @@
package com.starry.common.config;
import com.starry.common.interceptor.RequestLoggingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 日志记录配置类
* 配置请求日志拦截器
*
* 注意CorrelationFilter已移至play-admin模块通过@Component自动注册
*
* @author Claude
*/
@Configuration
public class LoggingConfig implements WebMvcConfigurer {
@Autowired
private RequestLoggingInterceptor requestLoggingInterceptor;
/**
* 添加请求日志拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLoggingInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/static/**",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/swagger-ui.html/**",
"/doc.html/**",
"/error",
"/favicon.ico"
);
}
}

View File

@@ -0,0 +1,210 @@
package com.starry.common.interceptor;
import com.alibaba.fastjson2.JSON;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 请求响应日志拦截器
* 记录HTTP请求和响应的详细信息用于调试和监控
*
* @author Claude
*/
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
private static final String START_TIME_ATTRIBUTE = "startTime";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute(START_TIME_ATTRIBUTE, startTime);
// 记录请求开始
logRequestStart(request);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
Long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE);
long duration = startTime != null ? System.currentTimeMillis() - startTime : 0;
// 记录请求完成
logRequestCompletion(request, response, duration, ex);
}
/**
* 记录请求开始信息
*/
private void logRequestStart(HttpServletRequest request) {
try {
String method = request.getMethod();
String uri = request.getRequestURI();
String queryString = request.getQueryString();
String remoteAddr = getClientIpAddress(request);
String userAgent = request.getHeader("User-Agent");
// 构建完整URL
String fullUrl = uri;
if (queryString != null && !queryString.isEmpty()) {
fullUrl += "?" + queryString;
}
log.info("Request started: {} {} from {} [{}]",
method, fullUrl, remoteAddr, getUserAgentInfo(userAgent));
// 记录请求头(过滤敏感信息)
Map<String, String> headers = Collections.list(request.getHeaderNames())
.stream()
.filter(this::isSafeHeader)
.collect(Collectors.toMap(
name -> name,
request::getHeader
));
if (!headers.isEmpty()) {
log.debug("Request headers: {}", JSON.toJSONString(headers));
}
} catch (Exception e) {
log.warn("Error logging request start: {}", e.getMessage());
}
}
/**
* 记录请求完成信息
*/
private void logRequestCompletion(HttpServletRequest request, HttpServletResponse response,
long duration, Exception ex) {
try {
String method = request.getMethod();
String uri = request.getRequestURI();
int status = response.getStatus();
String statusText = getStatusText(status);
if (ex != null) {
log.error("Request completed with error: {} {} - {} {} ({}ms) - Exception: {}",
method, uri, status, statusText, duration, ex.getMessage());
} else if (status >= 400) {
log.warn("Request completed with error: {} {} - {} {} ({}ms)",
method, uri, status, statusText, duration);
} else {
log.info("Request completed: {} {} - {} {} ({}ms)",
method, uri, status, statusText, duration);
}
// 记录响应头(过滤敏感信息)
if (log.isDebugEnabled()) {
Map<String, String> responseHeaders = response.getHeaderNames()
.stream()
.filter(this::isSafeHeader)
.collect(Collectors.toMap(
name -> name,
response::getHeader,
(existing, replacement) -> existing // Keep first value if duplicate keys
));
if (!responseHeaders.isEmpty()) {
log.debug("Response headers: {}", JSON.toJSONString(responseHeaders));
}
}
} catch (Exception e) {
log.warn("Error logging request completion: {}", e.getMessage());
}
}
/**
* 获取客户端真实IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String[] headerNames = {
"X-Forwarded-For",
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String headerName : headerNames) {
String ip = request.getHeader(headerName);
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
// X-Forwarded-For可能包含多个IP取第一个
if (ip.contains(",")) {
ip = ip.substring(0, ip.indexOf(",")).trim();
}
return ip;
}
}
return request.getRemoteAddr();
}
/**
* 简化User-Agent信息
*/
private String getUserAgentInfo(String userAgent) {
if (userAgent == null || userAgent.isEmpty()) {
return "Unknown";
}
if (userAgent.contains("WeChat")) {
return "WeChat";
} else if (userAgent.contains("Chrome")) {
return "Chrome";
} else if (userAgent.contains("Safari")) {
return "Safari";
} else if (userAgent.contains("Firefox")) {
return "Firefox";
} else if (userAgent.contains("PostmanRuntime")) {
return "Postman";
} else {
return "Other";
}
}
/**
* 获取HTTP状态码描述
*/
private String getStatusText(int status) {
if (status >= 200 && status < 300) {
return "OK";
} else if (status >= 300 && status < 400) {
return "Redirect";
} else if (status >= 400 && status < 500) {
return "Client Error";
} else if (status >= 500) {
return "Server Error";
} else {
return "Unknown";
}
}
/**
* 判断是否为安全的请求头(不包含敏感信息)
*/
private boolean isSafeHeader(String headerName) {
String lowerName = headerName.toLowerCase();
return !lowerName.contains("authorization") &&
!lowerName.contains("cookie") &&
!lowerName.contains("password") &&
!lowerName.contains("token") &&
!lowerName.contains("secret");
}
}

55
pom.xml
View File

@@ -50,7 +50,7 @@
<easyexcel.version>2.2.11</easyexcel.version>
<!-- weichat-->
<weixin-java.version>4.6.0</weixin-java.version>
<ws-schild.version>3.1.1</ws-schild.version>
<ws-schild.version>3.5.0</ws-schild.version>
<!-- Lombok -->
<lombok.version>1.18.30</lombok.version>
<!-- Flyway -->
@@ -225,6 +225,13 @@
<artifactId>jave-nativebin-linux64</artifactId>
<version>${ws-schild.version}</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osxm1</artifactId>
<version>${ws-schild.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -249,6 +256,14 @@
</dependencyManagement>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<!-- Spotless Maven Plugin for Code Formatting -->
<plugin>
@@ -316,4 +331,42 @@
-->
</plugins>
</build>
<profiles>
<!-- macOS Apple Silicon -->
<profile>
<id>osx-arm64</id>
<activation>
<os>
<name>Mac OS X</name>
<arch>aarch64</arch>
</os>
</activation>
<dependencies>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osxm1</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
<!-- 部署容器Linux x86_64 -->
<profile>
<id>linux-x86_64</id>
<activation>
<os>
<name>Linux</name>
<arch>amd64</arch>
</os>
</activation>
<dependencies>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

104
push-docker.sh Executable file
View File

@@ -0,0 +1,104 @@
#!/bin/bash
# 推送 Docker 镜像到私有仓库脚本
set -euo pipefail
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 私有仓库配置 (来自 README.md)
REGISTRY_URL="docker-registry.julyhaven.com"
REGISTRY_USER="hucs"
REGISTRY_PASS="hucsdev"
PROJECT_PATH="peipei/backend"
echo -e "${GREEN}=== 推送 PeiPei 后端镜像到私有仓库 ===${NC}"
# 获取 UTC+8 时间戳
TIMESTAMP=$(TZ='Asia/Shanghai' date +"%Y-%m-%d-%Hh-%Mm")
echo -e "${YELLOW}推送时间戳 (UTC+8): ${TIMESTAMP}${NC}"
# 本地镜像名称 (始终推送 amd64 版本到服务器)
LOCAL_IMAGE="peipei-backend"
LOCAL_TAG="latest-amd64" # 服务器使用 amd64 架构
# 远程镜像名称和标签
REMOTE_IMAGE="${REGISTRY_URL}/${PROJECT_PATH}"
VERSION_TAG="${TIMESTAMP}"
LATEST_TAG="latest"
echo -e "${BLUE}私有仓库地址: ${REGISTRY_URL}${NC}"
echo -e "${BLUE}项目路径: ${PROJECT_PATH}${NC}"
echo -e "${BLUE}远程镜像: ${REMOTE_IMAGE}${NC}"
# 检查本地镜像是否存在
if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${LOCAL_IMAGE}:${LOCAL_TAG}$"; then
echo -e "${RED}错误: 本地镜像 ${LOCAL_IMAGE}:${LOCAL_TAG} 不存在${NC}"
echo -e "${YELLOW}请先运行: ./build-docker.sh amd64${NC}"
echo -e "${YELLOW}当前可用的镜像:${NC}"
docker images --format "table {{.Repository}}\t{{.Tag}}" | grep "^${LOCAL_IMAGE}\s"
exit 1
fi
# 登录私有仓库
echo -e "${GREEN}登录私有 Docker 仓库...${NC}"
echo "${REGISTRY_PASS}" | docker login "${REGISTRY_URL}" -u "${REGISTRY_USER}" --password-stdin
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 登录私有仓库失败!${NC}"
exit 1
fi
echo -e "${GREEN}✅ 登录成功!${NC}"
# 标记镜像 - 版本标签
echo -e "${GREEN}标记镜像: ${LOCAL_IMAGE}:${LOCAL_TAG} -> ${REMOTE_IMAGE}:${VERSION_TAG}${NC}"
docker tag "${LOCAL_IMAGE}:${LOCAL_TAG}" "${REMOTE_IMAGE}:${VERSION_TAG}"
# 标记镜像 - latest 标签
echo -e "${GREEN}标记镜像: ${LOCAL_IMAGE}:${LOCAL_TAG} -> ${REMOTE_IMAGE}:${LATEST_TAG}${NC}"
docker tag "${LOCAL_IMAGE}:${LOCAL_TAG}" "${REMOTE_IMAGE}:${LATEST_TAG}"
# 推送版本标签
echo -e "${GREEN}推送版本标签镜像...${NC}"
docker push "${REMOTE_IMAGE}:${VERSION_TAG}"
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 推送版本标签失败!${NC}"
exit 1
fi
# 推送 latest 标签
echo -e "${GREEN}推送 latest 标签镜像...${NC}"
docker push "${REMOTE_IMAGE}:${LATEST_TAG}"
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 推送 latest 标签失败!${NC}"
exit 1
fi
echo -e "${GREEN}✅ 所有镜像推送成功!${NC}"
# 显示推送结果
echo -e "\n${BLUE}推送的镜像:${NC}"
echo -e " 📦 ${REMOTE_IMAGE}:${VERSION_TAG}"
echo -e " 📦 ${REMOTE_IMAGE}:${LATEST_TAG}"
echo -e "\n${YELLOW}服务器部署命令:${NC}"
echo -e " docker pull ${REMOTE_IMAGE}:${VERSION_TAG}"
echo -e " docker pull ${REMOTE_IMAGE}:${LATEST_TAG}"
echo -e "\n${YELLOW}更新 docker-compose.yml 中的镜像:${NC}"
echo -e " image: ${REMOTE_IMAGE}:${VERSION_TAG}"
echo -e "\n${BLUE}注意: 此脚本推送 amd64 架构镜像到服务器${NC}"
# 清理本地标记的远程镜像标签 (可选)
echo -e "\n${GREEN}清理本地远程标签...${NC}"
docker rmi "${REMOTE_IMAGE}:${VERSION_TAG}" "${REMOTE_IMAGE}:${LATEST_TAG}" 2>/dev/null || true
echo -e "\n${GREEN}🚀 推送完成!${NC}"

76
start.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
#这里可替换为你自己的执行程序,其他代码无需更改
APP_NAME=play-admin-1.0.jar
#使用说明,用来提示输入参数
usage() {
echo "Usage: sh 脚本名.sh [start|stop|restart|status]"
exit 1
}
#检查程序是否在运行
is_exist(){
pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}' `
#如果不存在返回1存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}
#启动方法
start(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is already running. pid=${pid} ."
else
nohup java -Dloader.path=./lib/ -Xms2g -Xmx2g -jar $APP_NAME --spring.profiles.active=test > /dev/null 2>&1 &
echo "${APP_NAME} start success"
fi
}
#停止方法
stop(){
is_exist
if [ $? -eq "0" ]; then
kill -9 $pid
else
echo "${APP_NAME} is not running"
fi
}
#输出运行状态
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is running. Pid is ${pid}"
else
echo "${APP_NAME} is NOT running."
fi
}
#重启
restart(){
stop
start
}
#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac