ソースを参照

智能柜项目提交

skyline 3 ヶ月 前
コミット
f269d530f1
47 ファイル変更1909 行追加0 行削除
  1. 2 0
      .gitignore
  2. BIN
      current_state.png
  3. BIN
      dashboard_screenshot.png
  4. BIN
      dashboard_verification.png
  5. BIN
      devtools_opened.png
  6. BIN
      final_menu_state.png
  7. 37 0
      haha-admin/.gitignore
  8. 121 0
      haha-admin/pom.xml
  9. 52 0
      haha-admin/src/main/java/com/haha/admin/AdminApplication.java
  10. 41 0
      haha-admin/src/main/java/com/haha/admin/config/GlobalExceptionHandler.java
  11. 48 0
      haha-admin/src/main/java/com/haha/admin/config/HahaSdkConfig.java
  12. 28 0
      haha-admin/src/main/java/com/haha/admin/config/PageHelperConfig.java
  13. 16 0
      haha-admin/src/main/java/com/haha/admin/config/RedisConfig.java
  14. 35 0
      haha-admin/src/main/java/com/haha/admin/config/SaTokenConfig.java
  15. 3 0
      haha-admin/src/main/java/com/haha/admin/constant/package-info.java
  16. 93 0
      haha-admin/src/main/java/com/haha/admin/controller/AdminController.java
  17. 70 0
      haha-admin/src/main/java/com/haha/admin/controller/AdminLoginController.java
  18. 276 0
      haha-admin/src/main/java/com/haha/admin/controller/OrderController.java
  19. 82 0
      haha-admin/src/main/java/com/haha/admin/controller/RoleController.java
  20. 19 0
      haha-admin/src/main/java/com/haha/admin/dto/AdminLoginDTO.java
  21. 27 0
      haha-admin/src/main/java/com/haha/admin/service/AdminLoginService.java
  22. 130 0
      haha-admin/src/main/java/com/haha/admin/service/impl/AdminLoginServiceImpl.java
  23. 3 0
      haha-admin/src/main/java/com/haha/admin/utils/package-info.java
  24. 85 0
      haha-admin/src/main/resources/application.yml
  25. 48 0
      haha-admin/src/main/resources/sql/admin.sql
  26. 12 0
      haha-admin/src/test/java/com/haha/admin/ApplicationTest.java
  27. 36 0
      haha-admin/src/test/java/com/haha/admin/PasswordEncoderTest.java
  28. 88 0
      haha-entity/src/main/java/com/haha/entity/Admin.java
  29. 45 0
      haha-entity/src/main/java/com/haha/entity/Role.java
  30. 12 0
      haha-mapper/src/main/java/com/haha/mapper/AdminMapper.java
  31. 30 0
      haha-mapper/src/main/java/com/haha/mapper/RoleMapper.java
  32. 6 0
      haha-service/compile.bat
  33. 61 0
      haha-service/src/main/java/com/haha/service/AdminService.java
  34. 51 0
      haha-service/src/main/java/com/haha/service/RoleService.java
  35. 209 0
      haha-service/src/main/java/com/haha/service/impl/AdminServiceImpl.java
  36. 142 0
      haha-service/src/main/java/com/haha/service/impl/RoleServiceImpl.java
  37. BIN
      menu_after_click.png
  38. BIN
      menu_after_hover.png
  39. BIN
      menu_api_response.png
  40. BIN
      menu_current_state.png
  41. BIN
      menu_diagnostic_report.png
  42. BIN
      menu_expanded.png
  43. BIN
      menu_full_page.png
  44. 1 0
      pom.xml
  45. BIN
      sidebar_detail.png
  46. BIN
      sidebar_full_check.png
  47. BIN
      sidebar_narrow.png

+ 2 - 0
.gitignore

@@ -20,3 +20,5 @@ haha-common/target/
 haha-entity/target/
 haha-mapper/target/
 haha-service/target/
+.qoder/
+.trae/

BIN
current_state.png


BIN
dashboard_screenshot.png


BIN
dashboard_verification.png


BIN
devtools_opened.png


BIN
final_menu_state.png


+ 37 - 0
haha-admin/.gitignore

@@ -0,0 +1,37 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Logs ###
+logs/
+*.log

+ 121 - 0
haha-admin/pom.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.haha</groupId>
+        <artifactId>haha-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>com.haha</groupId>
+    <artifactId>haha-admin</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>haha-admin</name>
+    <description>运营平台后端</description>
+
+
+
+    <dependencies>
+        <!-- 内部模块依赖 -->
+        <dependency>
+            <groupId>com.haha</groupId>
+            <artifactId>haha-service</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.haha</groupId>
+            <artifactId>haha-common</artifactId>
+        </dependency>
+
+        <!-- Spring Boot 核心依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- 数据库相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+        </dependency>
+
+        <!-- 工具类 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Redis 依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <!-- FastJson -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+        </dependency>
+
+        <!-- Sa-Token 认证框架 -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-spring-boot3-starter</artifactId>
+        </dependency>
+
+        <!-- Sa-Token 整合 Redis(使用 jackson 序列化) -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-redis-jackson</artifactId>
+        </dependency>
+
+        <!-- Spring Security Crypto (用于BCrypt密码加密) -->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-crypto</artifactId>
+        </dependency>
+
+        <!-- 提供 Redis 连接池 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <mainClass>com.haha.admin.AdminApplication</mainClass>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 52 - 0
haha-admin/src/main/java/com/haha/admin/AdminApplication.java

@@ -0,0 +1,52 @@
+package com.haha.admin;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.core.PriorityOrdered;
+import org.springframework.core.Ordered;
+
+@SpringBootApplication
+@ComponentScan(basePackages = {"com.haha"})
+@MapperScan("com.haha.mapper")
+public class AdminApplication {
+
+    @Bean
+    public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
+        return new PriorityOrderedBeanFactoryPostProcessor();
+    }
+
+    public static class PriorityOrderedBeanFactoryPostProcessor implements BeanFactoryPostProcessor, PriorityOrdered {
+        @Override
+        public int getOrder() {
+            return Ordered.HIGHEST_PRECEDENCE;
+        }
+
+        @Override
+        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+            for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
+                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
+                Object attr = beanDefinition.getAttribute("factoryBeanObjectType");
+                if (attr instanceof String) {
+                    try {
+                        // 将 String 类型的类名转换为真正的 Class 对象,解决 Spring 6.2+ (Boot 3.4/3.5) 的类型检查问题
+                        beanDefinition.setAttribute("factoryBeanObjectType", Class.forName((String) attr));
+                    } catch (Throwable e) {
+                        // 转换失败则移除该属性,由 Spring 自行推断
+                        beanDefinition.removeAttribute("factoryBeanObjectType");
+                    }
+                }
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(AdminApplication.class, args);
+    }
+}

+ 41 - 0
haha-admin/src/main/java/com/haha/admin/config/GlobalExceptionHandler.java

@@ -0,0 +1,41 @@
+package com.haha.admin.config;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import com.haha.common.vo.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    /**
+     * 处理 Sa-Token 未登录异常
+     */
+    @ExceptionHandler(NotLoginException.class)
+    public Result<Void> handleNotLoginException(NotLoginException e) {
+        log.warn("未登录异常: {}", e.getMessage());
+        // 根据异常类型返回不同的错误信息
+        String message;
+        if (e.getType().equals(NotLoginException.NOT_TOKEN)) {
+            message = "未提供token";
+        } else if (e.getType().equals(NotLoginException.INVALID_TOKEN)) {
+            message = "token无效";
+        } else if (e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
+            message = "token已过期";
+        } else {
+            message = "未登录";
+        }
+        return Result.error(401, message);
+    }
+
+    /**
+     * 处理其他所有异常
+     */
+    @ExceptionHandler(Exception.class)
+    public Result<Void> handleException(Exception e) {
+        log.error("系统异常: {}", e.getMessage(), e);
+        return Result.error(500, "系统内部错误");
+    }
+}

+ 48 - 0
haha-admin/src/main/java/com/haha/admin/config/HahaSdkConfig.java

@@ -0,0 +1,48 @@
+package com.haha.admin.config;
+
+import com.haha.sdk.HahaClient;
+import com.haha.sdk.config.HahaConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 哈哈零售 SDK 配置类
+ * 基于新版SDK重新配置
+ */
+@Configuration
+public class HahaSdkConfig {
+
+    @Value("${haha.api.base-url}")
+    private String apiBaseUrl;
+
+    @Value("${haha.api.app-id}")
+    private String appId;
+
+    @Value("${haha.api.app-secret}")
+    private String appSecret;
+
+    /**
+     * 创建哈哈零售SDK客户端Bean
+     * 注意:新版SDK包路径已更改为 com.haha.sdk.HahaClient
+     */
+    @Bean
+    public HahaClient hahaClient() {
+        // 创建SDK配置
+        HahaConfig config = HahaConfig.builder()
+                .apiBaseUrl(apiBaseUrl)  // 新SDK使用apiBaseUrl
+                .appId(appId)
+                .appSecret(appSecret)
+                .connectTimeout(10)
+                .readTimeout(30)
+                .writeTimeout(30)
+                .enableLog(true)
+                .build();
+        
+        // 校验配置
+        config.validate();
+        
+        // 创建客户端
+        return new HahaClient(config);
+    }
+}

+ 28 - 0
haha-admin/src/main/java/com/haha/admin/config/PageHelperConfig.java

@@ -0,0 +1,28 @@
+package com.haha.admin.config;
+
+import com.github.pagehelper.PageInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import java.util.Properties;
+
+@Configuration
+public class PageHelperConfig {
+
+    @Bean
+    public PageInterceptor pageInterceptor() {
+        PageInterceptor pageInterceptor = new PageInterceptor();
+        Properties properties = new Properties();
+        // 数据库方言
+        properties.setProperty("helperDialect", "mysql");
+        // 分页合理化参数,默认false。当设置为true时,pageNum<=0时会查询第一页,pageNum>pages时会查询最后一页
+        properties.setProperty("reasonable", "true");
+        // 支持通过Mapper接口参数来传递分页参数
+        properties.setProperty("supportMethodsArguments", "true");
+        // 为了支持startPage(Object params)方法,增加了一个"params"参数来配置参数映射
+        properties.setProperty("params", "count=countSql");
+        // 总是返回PageInfo类型
+        properties.setProperty("returnPageInfo", "check");
+        pageInterceptor.setProperties(properties);
+        return pageInterceptor;
+    }
+}

+ 16 - 0
haha-admin/src/main/java/com/haha/admin/config/RedisConfig.java

@@ -0,0 +1,16 @@
+package com.haha.admin.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Redis 配置类
+ * Sa-Token 使用 Redis 作为持久化存储,通过 sa-token-redis-jackson 自动配置
+ * Redis 连接信息通过 application.yml 中的 spring.redis 配置
+ */
+@Configuration
+@Slf4j
+public class RedisConfig {
+    // Sa-Token 的 Redis 整合已由 sa-token-redis-jackson 依赖自动配置
+    // 无需额外配置,Spring Boot 会自动读取 application.yml 中的 Redis 配置
+}

+ 35 - 0
haha-admin/src/main/java/com/haha/admin/config/SaTokenConfig.java

@@ -0,0 +1,35 @@
+package com.haha.admin.config;
+
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Slf4j
+@Configuration
+public class SaTokenConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册Sa-Token拦截器,拦截所有路径
+        registry.addInterceptor(new SaInterceptor(handle -> {
+            // 记录token信息(用于调试)
+            String tokenValue = StpUtil.getTokenValue();
+            if (tokenValue != null && !tokenValue.isEmpty()) {
+                log.debug("当前请求token: {}, 是否登录: {}", tokenValue, StpUtil.isLogin());
+            } else {
+                log.debug("当前请求未携带token");
+            }
+
+            // 登录认证:除了指定的接口,其他都需要登录
+            StpUtil.checkLogin();
+        }))
+        .addPathPatterns("/**")
+        .excludePathPatterns(
+            "/login/**",           // 登录接口
+            "/health/**"           // 健康检查接口
+        );
+    }
+}

+ 3 - 0
haha-admin/src/main/java/com/haha/admin/constant/package-info.java

@@ -0,0 +1,3 @@
+package com.haha.admin.constant;
+
+// 常量类包

+ 93 - 0
haha-admin/src/main/java/com/haha/admin/controller/AdminController.java

@@ -0,0 +1,93 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.service.AdminService;
+import com.haha.common.vo.Result;
+import com.haha.entity.Admin;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 管理员管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/user")
+public class AdminController {
+    
+    @Autowired
+    private AdminService adminService;
+    
+    /**
+     * 分页查询管理员列表
+     */
+    @PostMapping
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+        int page = Integer.parseInt(params.getOrDefault("page", 1).toString());
+        int pageSize = Integer.parseInt(params.getOrDefault("pageSize", 10).toString());
+        String username = (String) params.get("username");
+        String phone = (String) params.get("phone");
+        Long roleId = params.get("roleId") != null ? Long.valueOf(params.get("roleId").toString()) : null;
+        Integer status = params.get("status") != null ? Integer.valueOf(params.get("status").toString()) : null;
+        
+        Result<IPage<Admin>> result = adminService.getAdminList(page, pageSize, username, phone, roleId, status);
+        
+        // 转换为前端需要的格式
+        if (result.getCode() == 200 && result.getData() != null) {
+            IPage<Admin> pageData = result.getData();
+            Map<String, Object> data = Map.of(
+                "list", pageData.getRecords(),
+                "total", pageData.getTotal(),
+                "pageSize", pageData.getSize(),
+                "currentPage", pageData.getCurrent()
+            );
+            return Result.success("查询成功", data);
+        }
+        return Result.error(result.getCode(), "查询失败");
+    }
+    
+    /**
+     * 添加管理员
+     */
+    @PostMapping("/add")
+    public Result<Void> add(@RequestBody Admin admin) {
+        return adminService.addAdmin(admin);
+    }
+    
+    /**
+     * 更新管理员
+     */
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody Admin admin) {
+        return adminService.updateAdmin(admin);
+    }
+    
+    /**
+     * 删除管理员
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        return adminService.deleteAdmin(id);
+    }
+    
+    /**
+     * 重置密码
+     */
+    @PostMapping("/reset-password")
+    public Result<Void> resetPassword(@RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        String newPassword = (String) params.get("newPassword");
+        return adminService.resetPassword(id, newPassword);
+    }
+    
+    /**
+     * 获取统计数据
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> statistics() {
+        return adminService.getStatistics();
+    }
+}

+ 70 - 0
haha-admin/src/main/java/com/haha/admin/controller/AdminLoginController.java

@@ -0,0 +1,70 @@
+package com.haha.admin.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.stp.StpUtil;
+import com.haha.admin.dto.AdminLoginDTO;
+import com.haha.admin.service.AdminLoginService;
+import com.haha.common.vo.LoginVO;
+import com.haha.common.vo.Result;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 管理员登录控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/login")
+public class AdminLoginController {
+    
+    @Autowired
+    private AdminLoginService adminLoginService;
+    
+    /**
+     * 管理员登录
+     * @param loginDTO 登录请求参数
+     * @return 登录结果,包含token和用户信息
+     */
+    @SaIgnore
+    @PostMapping
+    public Result<LoginVO> login(@RequestBody AdminLoginDTO loginDTO) {
+        log.info("收到登录请求: username={}", loginDTO.getUsername());
+        return adminLoginService.login(loginDTO.getUsername(), loginDTO.getPassword());
+    }
+    
+    /**
+     * 退出登录
+     * @return 退出结果
+     */
+    @PostMapping("/logout")
+    public Result<Void> logout() {
+        try {
+            Object loginId = StpUtil.getLoginId();
+            log.info("管理员退出登录: userId={}", loginId);
+            StpUtil.logout();
+            return Result.success("退出登录成功", null);
+        } catch (Exception e) {
+            log.error("退出登录异常: {}", e.getMessage(), e);
+            return Result.error(500, "退出登录失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取当前登录管理员信息
+     * @return 管理员信息
+     */
+    @GetMapping("/info")
+    public Result<Map<String, Object>> getInfo() {
+        try {
+            Object loginId = StpUtil.getLoginId();
+            log.debug("获取管理员信息: userId={}", loginId);
+            return adminLoginService.getAdminInfo(loginId.toString());
+        } catch (Exception e) {
+            log.error("获取管理员信息异常: {}", e.getMessage(), e);
+            return Result.error(500, "获取信息失败: " + e.getMessage());
+        }
+    }
+}

+ 276 - 0
haha-admin/src/main/java/com/haha/admin/controller/OrderController.java

@@ -0,0 +1,276 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.haha.common.vo.Result;
+import com.haha.entity.Order;
+import com.haha.service.OrderService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 订单管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/order")
+public class OrderController {
+    
+    @Autowired
+    private OrderService orderService;
+    
+    /**
+     * 分页查询订单列表
+     * @param params 查询参数
+     * @return 订单列表
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(@RequestParam Map<String, Object> params) {
+        try {
+            // 分页参数(处理空字符串)
+            int page = 1;
+            int pageSize = 10;
+            
+            Object pageObj = params.get("page");
+            if (pageObj != null && !pageObj.toString().isEmpty()) {
+                page = Integer.parseInt(pageObj.toString());
+            }
+            
+            Object pageSizeObj = params.get("pageSize");
+            if (pageSizeObj != null && !pageSizeObj.toString().isEmpty()) {
+                pageSize = Integer.parseInt(pageSizeObj.toString());
+            }
+            
+            // 查询条件
+            String orderNo = (String) params.get("orderNo");
+            String deviceId = (String) params.get("deviceId");
+            String payStatus = (String) params.get("payStatus");
+            Integer status = null;
+            Object statusObj = params.get("status");
+            if (statusObj != null && !statusObj.toString().isEmpty()) {
+                status = Integer.valueOf(statusObj.toString());
+            }
+            String startDate = (String) params.get("startDate");
+            String endDate = (String) params.get("endDate");
+            
+            // 构建查询条件
+            LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
+            wrapper.like(orderNo != null && !orderNo.isEmpty(), Order::getOrderNo, orderNo)
+                   .like(deviceId != null && !deviceId.isEmpty(), Order::getDeviceId, deviceId)
+                   .eq(payStatus != null && !payStatus.isEmpty(), Order::getPayStatus, payStatus)
+                   .eq(status != null, Order::getStatus, status);
+            
+            // 时间范围查询
+            if (startDate != null && !startDate.isEmpty()) {
+                LocalDateTime startDateTime = LocalDate.parse(startDate).atStartOfDay();
+                wrapper.ge(Order::getCreateTime, startDateTime);
+            }
+            if (endDate != null && !endDate.isEmpty()) {
+                LocalDateTime endDateTime = LocalDate.parse(endDate).atTime(LocalTime.MAX);
+                wrapper.le(Order::getCreateTime, endDateTime);
+            }
+            
+            // 按创建时间倒序
+            wrapper.orderByDesc(Order::getCreateTime);
+            
+            // 分页查询
+            Page<Order> pageResult = new Page<>(page, pageSize);
+            IPage<Order> orderPage = orderService.page(pageResult, wrapper);
+            
+            // 填充标签字段
+            for (Order order : orderPage.getRecords()) {
+                fillOrderLabels(order);
+            }
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("list", orderPage.getRecords());
+            data.put("total", orderPage.getTotal());
+            data.put("pageSize", orderPage.getSize());
+            data.put("currentPage", orderPage.getCurrent());
+            
+            return Result.success("查询成功", data);
+            
+        } catch (Exception e) {
+            log.error("查询订单列表失败: {}", e.getMessage(), e);
+            return Result.error(500, "查询订单列表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取订单详情
+     * @param id 订单ID
+     * @return 订单详情
+     */
+    @GetMapping("/{id}")
+    public Result<Order> getById(@PathVariable Long id) {
+        try {
+            Order order = orderService.getById(id);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            fillOrderLabels(order);
+            return Result.success("查询成功", order);
+        } catch (Exception e) {
+            log.error("查询订单详情失败: orderId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "查询订单详情失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取订单统计数据
+     * @return 统计数据
+     */
+    @GetMapping("/statistics")
+    public Result<Map<String, Object>> getStatistics() {
+        try {
+            Map<String, Object> statistics = new HashMap<>();
+            
+            // 总订单数
+            long totalOrders = orderService.count();
+            statistics.put("totalOrders", totalOrders);
+            
+            // 今日订单数
+            LocalDateTime todayStart = LocalDate.now().atStartOfDay();
+            long todayOrders = orderService.lambdaQuery()
+                    .ge(Order::getCreateTime, todayStart)
+                    .count();
+            statistics.put("todayOrders", todayOrders);
+            
+            // 各状态订单数
+            long unpaidOrders = orderService.lambdaQuery()
+                    .eq(Order::getPayStatus, Order.PAY_STATUS_未支付)
+                    .count();
+            statistics.put("unpaidOrders", unpaidOrders);
+            
+            long completedOrders = orderService.lambdaQuery()
+                    .eq(Order::getStatus, Order.ORDER_STATUS_已完成)
+                    .count();
+            statistics.put("completedOrders", completedOrders);
+            
+            long cancelledOrders = orderService.lambdaQuery()
+                    .eq(Order::getStatus, Order.ORDER_STATUS_已取消)
+                    .count();
+            statistics.put("cancelledOrders", cancelledOrders);
+            
+            long refundOrders = orderService.lambdaQuery()
+                    .eq(Order::getPayStatus, Order.PAY_STATUS_已退款)
+                    .count();
+            statistics.put("refundOrders", refundOrders);
+            
+            // 总销售额(已完成订单)
+            Double totalAmount = orderService.lambdaQuery()
+                    .eq(Order::getStatus, Order.ORDER_STATUS_已完成)
+                    .eq(Order::getPayStatus, Order.PAY_STATUS_已支付)
+                    .list()
+                    .stream()
+                    .mapToDouble(Order::getTotalAmount)
+                    .sum();
+            statistics.put("totalAmount", String.format("%.2f", totalAmount));
+            
+            // 今日销售额
+            Double todayAmount = orderService.lambdaQuery()
+                    .ge(Order::getCreateTime, todayStart)
+                    .eq(Order::getStatus, Order.ORDER_STATUS_已完成)
+                    .eq(Order::getPayStatus, Order.PAY_STATUS_已支付)
+                    .list()
+                    .stream()
+                    .mapToDouble(Order::getTotalAmount)
+                    .sum();
+            statistics.put("todayAmount", String.format("%.2f", todayAmount));
+            
+            // 平均订单金额
+            double averageAmount = completedOrders > 0 ? totalAmount / completedOrders : 0;
+            statistics.put("averageAmount", String.format("%.2f", averageAmount));
+            
+            return Result.success("查询成功", statistics);
+            
+        } catch (Exception e) {
+            log.error("查询订单统计失败: {}", e.getMessage(), e);
+            return Result.error(500, "查询订单统计失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 订单退款
+     * @param id 订单ID
+     * @param params 退款参数
+     * @return 退款结果
+     */
+    @PostMapping("/{id}/refund")
+    public Result<Void> refund(@PathVariable Long id, @RequestBody Map<String, Object> params) {
+        try {
+            Order order = orderService.getById(id);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            
+            // 检查订单状态
+            if (!Order.PAY_STATUS_已支付.equals(order.getPayStatus())) {
+                return Result.error(400, "只有已支付的订单才能退款");
+            }
+            
+            String reason = (String) params.get("reason");
+            log.info("订单退款: orderId={}, orderNo={}, reason={}", id, order.getOrderNo(), reason);
+            
+            // 更新支付状态为已退款
+            boolean success = orderService.updatePayStatus(order.getOrderNo(), Order.PAY_STATUS_已退款);
+            
+            if (success) {
+                return Result.success("退款成功", null);
+            } else {
+                return Result.error(500, "退款失败");
+            }
+            
+        } catch (Exception e) {
+            log.error("订单退款失败: orderId={}, error={}", id, e.getMessage(), e);
+            return Result.error(500, "退款失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 填充订单标签字段
+     * @param order 订单对象
+     */
+    private void fillOrderLabels(Order order) {
+        // 填充支付状态标签
+        switch (order.getPayStatus()) {
+            case Order.PAY_STATUS_未支付:
+                order.setPayStatusLabel("未支付");
+                break;
+            case Order.PAY_STATUS_已支付:
+                order.setPayStatusLabel("已支付");
+                break;
+            case Order.PAY_STATUS_已退款:
+                order.setPayStatusLabel("已退款");
+                break;
+            default:
+                order.setPayStatusLabel("未知");
+        }
+        
+        // 填充订单状态标签
+        switch (order.getStatus()) {
+            case Order.ORDER_STATUS_已取消:
+                order.setStatusLabel("已取消");
+                break;
+            case Order.ORDER_STATUS_待支付:
+                order.setStatusLabel("待支付");
+                break;
+            case Order.ORDER_STATUS_已完成:
+                order.setStatusLabel("已完成");
+                break;
+            case Order.ORDER_STATUS_已关闭:
+                order.setStatusLabel("已关闭");
+                break;
+            default:
+                order.setStatusLabel("未知");
+        }
+    }
+}

+ 82 - 0
haha-admin/src/main/java/com/haha/admin/controller/RoleController.java

@@ -0,0 +1,82 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.service.RoleService;
+import com.haha.common.vo.Result;
+import com.haha.entity.Role;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 角色管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/role")
+public class RoleController {
+    
+    @Autowired
+    private RoleService roleService;
+    
+    /**
+     * 分页查询角色列表
+     */
+    @PostMapping
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+        int page = Integer.parseInt(params.getOrDefault("page", 1).toString());
+        int pageSize = Integer.parseInt(params.getOrDefault("pageSize", 10).toString());
+        String name = (String) params.get("name");
+        String code = (String) params.get("code");
+        
+        Result<IPage<Role>> result = roleService.getRoleList(page, pageSize, name, code);
+        
+        // 转换为前端需要的格式
+        if (result.getCode() == 200 && result.getData() != null) {
+            IPage<Role> pageData = result.getData();
+            Map<String, Object> data = Map.of(
+                "list", pageData.getRecords(),
+                "total", pageData.getTotal(),
+                "pageSize", pageData.getSize(),
+                "currentPage", pageData.getCurrent()
+            );
+            return Result.success("查询成功", data);
+        }
+        return Result.error(result.getCode(), "查询失败");
+    }
+    
+    /**
+     * 获取所有角色(不分页)
+     */
+    @GetMapping("/list-all-role")
+    public Result<List<Role>> listAll() {
+        return roleService.getAllRoles();
+    }
+    
+    /**
+     * 添加角色
+     */
+    @PostMapping("/add")
+    public Result<Void> add(@RequestBody Role role) {
+        return roleService.addRole(role);
+    }
+    
+    /**
+     * 更新角色
+     */
+    @PostMapping("/update")
+    public Result<Void> update(@RequestBody Role role) {
+        return roleService.updateRole(role);
+    }
+    
+    /**
+     * 删除角色
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        return roleService.deleteRole(id);
+    }
+}

+ 19 - 0
haha-admin/src/main/java/com/haha/admin/dto/AdminLoginDTO.java

@@ -0,0 +1,19 @@
+package com.haha.admin.dto;
+
+import lombok.Data;
+
+/**
+ * 管理员登录请求DTO
+ */
+@Data
+public class AdminLoginDTO {
+    /**
+     * 用户名
+     */
+    private String username;
+    
+    /**
+     * 密码
+     */
+    private String password;
+}

+ 27 - 0
haha-admin/src/main/java/com/haha/admin/service/AdminLoginService.java

@@ -0,0 +1,27 @@
+package com.haha.admin.service;
+
+import com.haha.common.vo.LoginVO;
+import com.haha.common.vo.Result;
+
+import java.util.Map;
+
+/**
+ * 管理员登录服务接口
+ */
+public interface AdminLoginService {
+    
+    /**
+     * 管理员账号密码登录
+     * @param username 用户名
+     * @param password 密码
+     * @return 登录结果,包含token和用户信息
+     */
+    Result<LoginVO> login(String username, String password);
+    
+    /**
+     * 获取当前登录管理员信息
+     * @param adminId 管理员ID
+     * @return 管理员信息
+     */
+    Result<Map<String, Object>> getAdminInfo(String adminId);
+}

+ 130 - 0
haha-admin/src/main/java/com/haha/admin/service/impl/AdminLoginServiceImpl.java

@@ -0,0 +1,130 @@
+package com.haha.admin.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.haha.admin.service.AdminLoginService;
+import com.haha.common.vo.LoginVO;
+import com.haha.common.vo.Result;
+import com.haha.common.vo.UserVO;
+import com.haha.entity.Admin;
+import com.haha.mapper.AdminMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 管理员登录服务实现
+ */
+@Slf4j
+@Service
+public class AdminLoginServiceImpl implements AdminLoginService {
+    
+    @Autowired
+    private AdminMapper adminMapper;
+    
+    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+    
+    @Override
+    public Result<LoginVO> login(String username, String password) {
+        try {
+            log.info("管理员登录尝试: username={}", username);
+            
+            // 参数校验
+            if (username == null || username.trim().isEmpty()) {
+                return Result.error(400, "用户名不能为空");
+            }
+            if (password == null || password.trim().isEmpty()) {
+                return Result.error(400, "密码不能为空");
+            }
+            
+            // 查询管理员信息
+            LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(Admin::getUsername, username);
+            Admin admin = adminMapper.selectOne(wrapper);
+            
+            if (admin == null) {
+                log.warn("登录失败: 用户名不存在 username={}", username);
+                return Result.error(401, "用户名或密码错误");
+            }
+            
+            // 检查账号状态
+            if (admin.getStatus() != 1) {
+                log.warn("登录失败: 账号已禁用 username={}", username);
+                return Result.error(403, "账号已被禁用,请联系管理员");
+            }
+            
+            // 验证密码
+            if (!passwordEncoder.matches(password, admin.getPassword())) {
+                log.warn("登录失败: 密码错误 username={}", username);
+                return Result.error(401, "用户名或密码错误");
+            }
+            
+            // 更新最后登录时间和IP(这里简化处理,实际应从HttpServletRequest获取IP)
+            admin.setLastLoginTime(LocalDateTime.now());
+            admin.setLastLoginIp("127.0.0.1");
+            adminMapper.updateById(admin);
+            
+            // 使用Sa-Token进行登录,生成token
+            StpUtil.login(admin.getId());
+            String token = StpUtil.getTokenValue();
+            
+            // 在Session中存储用户信息
+            StpUtil.getSession().set("username", admin.getUsername());
+            StpUtil.getSession().set("realName", admin.getRealName());
+            StpUtil.getSession().set("roleIds", admin.getRoleIds());
+            
+            log.info("管理员登录成功: username={}, userId={}", username, admin.getId());
+            
+            // 构建返回数据
+            UserVO userVO = UserVO.builder()
+                    .id(admin.getId())
+                    .nickname(admin.getRealName() != null ? admin.getRealName() : admin.getUsername())
+                    .avatar(admin.getAvatar())
+                    .phone(admin.getPhone())
+                    .build();
+            
+            LoginVO loginVO = LoginVO.builder()
+                    .token(token)
+                    .userInfo(userVO)
+                    .build();
+            
+            return Result.success("登录成功", loginVO);
+            
+        } catch (Exception e) {
+            log.error("登录异常: username={}, error={}", username, e.getMessage(), e);
+            return Result.error(500, "登录失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Map<String, Object>> getAdminInfo(String adminId) {
+        try {
+            // 查询管理员信息
+            Admin admin = adminMapper.selectById(adminId);
+            if (admin == null) {
+                return Result.error(404, "管理员不存在");
+            }
+            
+            Map<String, Object> info = new HashMap<>();
+            info.put("id", admin.getId());
+            info.put("username", admin.getUsername());
+            info.put("realName", admin.getRealName());
+            info.put("phone", admin.getPhone());
+            info.put("email", admin.getEmail());
+            info.put("avatar", admin.getAvatar());
+            info.put("department", admin.getDepartment());
+            info.put("roleIds", admin.getRoleIds());
+            
+            return Result.success("获取成功", info);
+            
+        } catch (Exception e) {
+            log.error("获取管理员信息异常: adminId={}, error={}", adminId, e.getMessage(), e);
+            return Result.error(500, "获取信息失败: " + e.getMessage());
+        }
+    }
+}

+ 3 - 0
haha-admin/src/main/java/com/haha/admin/utils/package-info.java

@@ -0,0 +1,3 @@
+package com.haha.admin.utils;
+
+// 工具类包

+ 85 - 0
haha-admin/src/main/resources/application.yml

@@ -0,0 +1,85 @@
+spring:
+  application:
+    name: haha-admin
+
+  # 数据库配置
+  datasource:
+    url: jdbc:mysql://server.kuaiyuman.cn:3306/haha?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
+    username: root
+    password: KuaiyuMan/*-
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+  # Redis 配置
+  data:
+    redis:
+      host: server.kuaiyuman.cn
+      port: 6379
+      password: KtXA^Zx!TZmLEy(@JjB@2(TVG0kdy5)&
+      database: 9
+      timeout: 10000
+      lettuce:
+        pool:
+          max-active: 8
+          max-wait: -1
+          max-idle: 8
+          min-idle: 0
+
+# MyBatis-Plus 配置
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true
+  global-config:
+    enable-sql-runner: false
+
+# PageHelper 分页插件配置
+pagehelper:
+  # 指定数据库方言
+  helper-dialect: mysql
+  # 分页合理化参数,默认false。当设置为true时,pageNum<=0时会查询第一页,pageNum>pages(超过总数时),会查询最后一页
+  reasonable: true
+  # 支持通过Mapper接口参数来传递分页参数,默认false
+  support-methods-arguments: true
+  # 为了支持startPage(Object params)方法,增加了一个"params"参数来配置参数映射,用于从Map或ServletRequest中取值
+  params: count=countSql
+  # 总是返回PageInfo类型,默认false
+  return-page-info: check
+
+# 服务器配置
+server:
+  port: 8080
+  servlet:
+    context-path: /admin
+
+# Sa-Token 配置
+sa-token:
+  # token 名称(同时也是 cookie 名称)
+  token-name: adminAccessToken
+  # token 有效期(单位:秒) - 8小时
+  timeout: 28800
+  # token 临时有效期(单位:秒)
+  activity-timeout: -1
+  # 是否允许同一账号多地同时登录
+  is-concurrent: false
+  # 同一账号最大登录数量
+  max-login-count: 1
+  # token 风格
+  token-style: uuid
+  # 是否输出操作日志
+  is-log: true
+
+# 哈哈零售 API 配置
+haha:
+  api:
+    app-id: 2601051549145878
+    app-secret: 06e1be59332b00de0baad82002cdbcb5
+    base-url: http://api.hahabianli.com/
+
+# 日志配置
+logging:
+  level:
+    root: info
+    com.haha.admin: debug
+  file:
+    # 日志文件路径(相对于项目启动目录)
+    path: ./logs
+    name: ./logs/haha-admin.log

+ 48 - 0
haha-admin/src/main/resources/sql/admin.sql

@@ -0,0 +1,48 @@
+-- 管理员表
+CREATE TABLE IF NOT EXISTS `t_admin` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `username` varchar(50) NOT NULL COMMENT '用户名',
+  `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)',
+  `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
+  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
+  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
+  `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL',
+  `department` varchar(100) DEFAULT NULL COMMENT '部门',
+  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-正常,2-禁用',
+  `role_ids` varchar(200) DEFAULT NULL COMMENT '角色ID列表(逗号分隔)',
+  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
+  `last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_username` (`username`),
+  KEY `idx_phone` (`phone`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';
+
+-- 插入测试管理员数据
+-- 密码都是: admin123 (BCrypt加密后的值)
+INSERT INTO `t_admin` (`id`, `username`, `password`, `real_name`, `phone`, `email`, `department`, `status`, `role_ids`) VALUES
+(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z2EHCDhn/n8mrEio3Knt2CCi', '系统管理员', '13800138000', 'admin@haha.com', '技术部', 1, '1'),
+(2, 'operator', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z2EHCDhn/n8mrEio3Knt2CCi', '运营人员', '13800138001', 'operator@haha.com', '运营部', 1, '2'),
+(3, 'service', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z2EHCDhn/n8mrEio3Knt2CCi', '客服人员', '13800138002', 'service@haha.com', '客服部', 1, '3');
+
+-- 角色表
+CREATE TABLE IF NOT EXISTS `t_admin_role` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
+  `role_code` varchar(50) NOT NULL COMMENT '角色编码',
+  `description` varchar(200) DEFAULT NULL COMMENT '角色描述',
+  `permissions` text COMMENT '权限列表(JSON格式)',
+  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-启用,2-禁用',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_role_code` (`role_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员角色表';
+
+-- 插入测试角色数据
+INSERT INTO `t_admin_role` (`id`, `role_name`, `role_code`, `description`, `permissions`) VALUES
+(1, '超级管理员', 'super_admin', '拥有系统所有权限', '["*:*:*"]'),
+(2, '运营人员', 'operator', '负责日常运营管理', '["order:*:*","device:*:*","store:*:*","product:*:*"]'),
+(3, '客服人员', 'service', '负责客户服务', '["order:view:*","order:refund:*"]');

+ 12 - 0
haha-admin/src/test/java/com/haha/admin/ApplicationTest.java

@@ -0,0 +1,12 @@
+package com.haha.admin;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ApplicationTest {
+
+    @Test
+    void contextLoads() {
+    }
+}

+ 36 - 0
haha-admin/src/test/java/com/haha/admin/PasswordEncoderTest.java

@@ -0,0 +1,36 @@
+package com.haha.admin;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+/**
+ * 密码加密工具测试类
+ * 用于生成BCrypt加密后的密码
+ */
+public class PasswordEncoderTest {
+    
+    @Test
+    public void generatePassword() {
+        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+        
+        // 生成密码: admin123
+        String password = "admin123";
+        String encodedPassword = encoder.encode(password);
+        
+        System.out.println("原始密码: " + password);
+        System.out.println("BCrypt加密后: " + encodedPassword);
+        System.out.println();
+        
+        // 生成SQL语句
+        System.out.println("-- 管理员表SQL");
+        System.out.println("-- 密码: admin123");
+        System.out.println("UPDATE t_admin SET password = '" + encodedPassword + "' WHERE username = 'admin';");
+        System.out.println("UPDATE t_admin SET password = '" + encodedPassword + "' WHERE username = 'operator';");
+        System.out.println("UPDATE t_admin SET password = '" + encodedPassword + "' WHERE username = 'service';");
+        System.out.println();
+        
+        // 验证密码
+        boolean matches = encoder.matches(password, encodedPassword);
+        System.out.println("密码验证结果: " + matches);
+    }
+}

+ 88 - 0
haha-entity/src/main/java/com/haha/entity/Admin.java

@@ -0,0 +1,88 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 管理员实体类
+ */
+@Data
+@TableName("t_admin")
+public class Admin implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 密码(BCrypt加密)
+     */
+    private String password;
+
+    /**
+     * 真实姓名
+     */
+    private String realName;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 邮箱
+     */
+    private String email;
+
+    /**
+     * 头像URL
+     */
+    private String avatar;
+
+    /**
+     * 部门
+     */
+    private String department;
+
+    /**
+     * 状态:1-正常,2-禁用
+     */
+    private Integer status;
+
+    /**
+     * 角色ID列表(逗号分隔)
+     */
+    private String roleIds;
+
+    /**
+     * 最后登录时间
+     */
+    private LocalDateTime lastLoginTime;
+
+    /**
+     * 最后登录IP
+     */
+    private String lastLoginIp;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 45 - 0
haha-entity/src/main/java/com/haha/entity/Role.java

@@ -0,0 +1,45 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 角色实体类
+ */
+@Data
+@TableName("t_role")
+public class Role implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 角色名称
+     */
+    private String name;
+
+    /**
+     * 角色编码
+     */
+    private String code;
+
+    /**
+     * 角色描述
+     */
+    private String description;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/AdminMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.Admin;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 管理员Mapper接口
+ */
+@Mapper
+public interface AdminMapper extends BaseMapper<Admin> {
+}

+ 30 - 0
haha-mapper/src/main/java/com/haha/mapper/RoleMapper.java

@@ -0,0 +1,30 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.Role;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 角色Mapper接口
+ */
+@Mapper
+public interface RoleMapper extends BaseMapper<Role> {
+    
+    /**
+     * 根据角色编码查询角色
+     * @param code 角色编码
+     * @return 角色信息
+     */
+    @Select("SELECT * FROM t_role WHERE code = #{code}")
+    Role selectByCode(@Param("code") String code);
+    
+    /**
+     * 统计该角色的用户数量
+     * @param roleId 角色ID
+     * @return 用户数量
+     */
+    @Select("SELECT COUNT(*) FROM t_admin WHERE role_id = #{roleId}")
+    int countUsersByRoleId(@Param("roleId") Long roleId);
+}

+ 6 - 0
haha-service/compile.bat

@@ -0,0 +1,6 @@
+@echo off
+if not exist "target\classes" mkdir "target\classes"
+set CLASSPATH=../haha-entity/target/classes;../haha-mapper/target/classes;lib/*
+javac -cp "%CLASSPATH%" -d "target\classes" src\main\java\com\haha\service\*.java src\main\java\com\haha\service\impl\*.java
+echo Compilation completed!
+pause

+ 61 - 0
haha-service/src/main/java/com/haha/service/AdminService.java

@@ -0,0 +1,61 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.common.vo.Result;
+import com.haha.entity.Admin;
+
+import java.util.Map;
+
+/**
+ * 管理员服务接口
+ */
+public interface AdminService extends IService<Admin> {
+    
+    /**
+     * 分页查询管理员列表
+     * @param page 当前页
+     * @param pageSize 每页大小
+     * @param username 用户名(模糊查询)
+     * @param phone 手机号
+     * @param roleId 角色ID
+     * @param status 状态
+     * @return 分页结果
+     */
+    Result<IPage<Admin>> getAdminList(int page, int pageSize, String username, String phone, Long roleId, Integer status);
+    
+    /**
+     * 添加管理员
+     * @param admin 管理员信息
+     * @return 添加结果
+     */
+    Result<Void> addAdmin(Admin admin);
+    
+    /**
+     * 更新管理员信息
+     * @param admin 管理员信息
+     * @return 更新结果
+     */
+    Result<Void> updateAdmin(Admin admin);
+    
+    /**
+     * 删除管理员
+     * @param id 管理员ID
+     * @return 删除结果
+     */
+    Result<Void> deleteAdmin(Long id);
+    
+    /**
+     * 重置密码
+     * @param id 管理员ID
+     * @param newPassword 新密码
+     * @return 重置结果
+     */
+    Result<Void> resetPassword(Long id, String newPassword);
+    
+    /**
+     * 获取统计数据
+     * @return 统计数据
+     */
+    Result<Map<String, Object>> getStatistics();
+}

+ 51 - 0
haha-service/src/main/java/com/haha/service/RoleService.java

@@ -0,0 +1,51 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.common.vo.Result;
+import com.haha.entity.Role;
+
+import java.util.List;
+
+/**
+ * 角色服务接口
+ */
+public interface RoleService extends IService<Role> {
+    
+    /**
+     * 分页查询角色列表
+     * @param page 当前页
+     * @param pageSize 每页大小
+     * @param name 角色名称(模糊查询)
+     * @param code 角色编码
+     * @return 分页结果
+     */
+    Result<IPage<Role>> getRoleList(int page, int pageSize, String name, String code);
+    
+    /**
+     * 获取所有角色列表(不分页)
+     * @return 角色列表
+     */
+    Result<List<Role>> getAllRoles();
+    
+    /**
+     * 添加角色
+     * @param role 角色信息
+     * @return 添加结果
+     */
+    Result<Void> addRole(Role role);
+    
+    /**
+     * 更新角色信息
+     * @param role 角色信息
+     * @return 更新结果
+     */
+    Result<Void> updateRole(Role role);
+    
+    /**
+     * 删除角色
+     * @param id 角色ID
+     * @return 删除结果
+     */
+    Result<Void> deleteRole(Long id);
+}

+ 209 - 0
haha-service/src/main/java/com/haha/service/impl/AdminServiceImpl.java

@@ -0,0 +1,209 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.vo.Result;
+import com.haha.entity.Admin;
+import com.haha.mapper.AdminMapper;
+import com.haha.service.AdminService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 管理员服务实现类
+ */
+@Slf4j
+@Service
+public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService {
+    
+    @Override
+    public Result<IPage<Admin>> getAdminList(int page, int pageSize, String username, String phone, Long roleId, Integer status) {
+        try {
+            LambdaQueryWrapper<Admin> wrapper = new LambdaQueryWrapper<>();
+            
+            // 条件查询
+            if (StringUtils.hasText(username)) {
+                wrapper.and(w -> w.like(Admin::getUsername, username)
+                                  .or().like(Admin::getRealName, username));
+            }
+            if (StringUtils.hasText(phone)) {
+                wrapper.eq(Admin::getPhone, phone);
+            }
+            if (roleId != null) {
+                wrapper.eq(Admin::getRoleId, roleId);
+            }
+            if (status != null) {
+                wrapper.eq(Admin::getStatus, status);
+            }
+            
+            // 按创建时间倒序
+            wrapper.orderByDesc(Admin::getCreateTime);
+            
+            // 分页查询
+            Page<Admin> pageObj = new Page<>(page, pageSize);
+            IPage<Admin> result = this.page(pageObj, wrapper);
+            
+            // 密码字段置空
+            result.getRecords().forEach(admin -> admin.setPassword(null));
+            
+            return Result.success("查询成功", result);
+            
+        } catch (Exception e) {
+            log.error("查询管理员列表失败", e);
+            return Result.error(500, "查询失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> addAdmin(Admin admin) {
+        try {
+            // 检查用户名是否已存在
+            Admin existAdmin = baseMapper.selectByUsername(admin.getUsername());
+            if (existAdmin != null) {
+                return Result.error(400, "用户名已存在");
+            }
+            
+            // 检查手机号是否已存在
+            if (StringUtils.hasText(admin.getPhone())) {
+                existAdmin = baseMapper.selectByPhone(admin.getPhone());
+                if (existAdmin != null) {
+                    return Result.error(400, "手机号已存在");
+                }
+            }
+            
+            // 设置默认值
+            admin.setStatus(1); // 默认正常状态
+            admin.setCreateTime(LocalDateTime.now());
+            admin.setUpdateTime(LocalDateTime.now());
+            
+            // 临时方案:密码明文存储,生产环境需要加密
+            if (!StringUtils.hasText(admin.getPassword())) {
+                admin.setPassword("123456"); // 默认密码
+            }
+            
+            boolean success = this.save(admin);
+            return success ? Result.success("添加成功", null) : Result.error(500, "添加失败");
+            
+        } catch (Exception e) {
+            log.error("添加管理员失败", e);
+            return Result.error(500, "添加失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> updateAdmin(Admin admin) {
+        try {
+            // 检查管理员是否存在
+            Admin existAdmin = this.getById(admin.getId());
+            if (existAdmin == null) {
+                return Result.error(404, "管理员不存在");
+            }
+            
+            // 如果修改了用户名,检查是否重复
+            if (!existAdmin.getUsername().equals(admin.getUsername())) {
+                Admin checkAdmin = baseMapper.selectByUsername(admin.getUsername());
+                if (checkAdmin != null) {
+                    return Result.error(400, "用户名已存在");
+                }
+            }
+            
+            // 如果修改了手机号,检查是否重复
+            if (StringUtils.hasText(admin.getPhone()) && !admin.getPhone().equals(existAdmin.getPhone())) {
+                Admin checkAdmin = baseMapper.selectByPhone(admin.getPhone());
+                if (checkAdmin != null) {
+                    return Result.error(400, "手机号已存在");
+                }
+            }
+            
+            admin.setUpdateTime(LocalDateTime.now());
+            // 不允许通过此接口修改密码
+            admin.setPassword(null);
+            
+            boolean success = this.updateById(admin);
+            return success ? Result.success("更新成功", null) : Result.error(500, "更新失败");
+            
+        } catch (Exception e) {
+            log.error("更新管理员失败", e);
+            return Result.error(500, "更新失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> deleteAdmin(Long id) {
+        try {
+            // 检查管理员是否存在
+            Admin admin = this.getById(id);
+            if (admin == null) {
+                return Result.error(404, "管理员不存在");
+            }
+            
+            // TODO: 检查是否可以删除(如果是超级管理员则不允许删除)
+            
+            boolean success = this.removeById(id);
+            return success ? Result.success("删除成功", null) : Result.error(500, "删除失败");
+            
+        } catch (Exception e) {
+            log.error("删除管理员失败", e);
+            return Result.error(500, "删除失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> resetPassword(Long id, String newPassword) {
+        try {
+            Admin admin = this.getById(id);
+            if (admin == null) {
+                return Result.error(404, "管理员不存在");
+            }
+            
+            // 临时方案:密码明文存储,生产环境需要加密
+            if (!StringUtils.hasText(newPassword)) {
+                newPassword = "123456"; // 默认密码
+            }
+            
+            admin.setPassword(newPassword);
+            admin.setUpdateTime(LocalDateTime.now());
+            
+            boolean success = this.updateById(admin);
+            return success ? Result.success("密码重置成功", null) : Result.error(500, "密码重置失败");
+            
+        } catch (Exception e) {
+            log.error("重置密码失败", e);
+            return Result.error(500, "重置密码失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Map<String, Object>> getStatistics() {
+        try {
+            Map<String, Object> statistics = new HashMap<>();
+            
+            // 总管理员数
+            long totalUsers = this.count();
+            statistics.put("totalUsers", totalUsers);
+            
+            // 正常状态管理员数
+            long activeUsers = this.count(new LambdaQueryWrapper<Admin>().eq(Admin::getStatus, 1));
+            statistics.put("activeUsers", activeUsers);
+            
+            // 在线用户数(暂时返回0,后续可通过Redis统计)
+            statistics.put("onlineUsers", 0);
+            
+            // 今日新增
+            statistics.put("todayNew", 0);
+            
+            return Result.success("获取成功", statistics);
+            
+        } catch (Exception e) {
+            log.error("获取统计数据失败", e);
+            return Result.error(500, "获取统计数据失败: " + e.getMessage());
+        }
+    }
+}

+ 142 - 0
haha-service/src/main/java/com/haha/service/impl/RoleServiceImpl.java

@@ -0,0 +1,142 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.vo.Result;
+import com.haha.entity.Role;
+import com.haha.mapper.RoleMapper;
+import com.haha.service.RoleService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 角色服务实现类
+ */
+@Slf4j
+@Service
+public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
+    
+    @Override
+    public Result<IPage<Role>> getRoleList(int page, int pageSize, String name, String code) {
+        try {
+            LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<>();
+            
+            // 条件查询
+            if (StringUtils.hasText(name)) {
+                wrapper.like(Role::getName, name);
+            }
+            if (StringUtils.hasText(code)) {
+                wrapper.eq(Role::getCode, code);
+            }
+            
+            // 按创建时间倒序
+            wrapper.orderByDesc(Role::getCreateTime);
+            
+            // 分页查询
+            Page<Role> pageObj = new Page<>(page, pageSize);
+            IPage<Role> result = this.page(pageObj, wrapper);
+            
+            // 为每个角色添加用户数量
+            result.getRecords().forEach(role -> {
+                int userCount = baseMapper.countUsersByRoleId(role.getId());
+                // 临时方案:将用户数量存储在description字段中,前端会单独获取
+            });
+            
+            return Result.success("查询成功", result);
+            
+        } catch (Exception e) {
+            log.error("查询角色列表失败", e);
+            return Result.error(500, "查询失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<List<Role>> getAllRoles() {
+        try {
+            List<Role> roles = this.list(new LambdaQueryWrapper<Role>().orderByAsc(Role::getId));
+            return Result.success("查询成功", roles);
+        } catch (Exception e) {
+            log.error("获取所有角色失败", e);
+            return Result.error(500, "查询失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> addRole(Role role) {
+        try {
+            // 检查角色编码是否已存在
+            Role existRole = baseMapper.selectByCode(role.getCode());
+            if (existRole != null) {
+                return Result.error(400, "角色编码已存在");
+            }
+            
+            // 设置创建时间
+            role.setCreateTime(LocalDateTime.now());
+            role.setUpdateTime(LocalDateTime.now());
+            
+            boolean success = this.save(role);
+            return success ? Result.success("添加成功", null) : Result.error(500, "添加失败");
+            
+        } catch (Exception e) {
+            log.error("添加角色失败", e);
+            return Result.error(500, "添加失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> updateRole(Role role) {
+        try {
+            // 检查角色是否存在
+            Role existRole = this.getById(role.getId());
+            if (existRole == null) {
+                return Result.error(404, "角色不存在");
+            }
+            
+            // 如果修改了编码,检查是否重复
+            if (!existRole.getCode().equals(role.getCode())) {
+                Role checkRole = baseMapper.selectByCode(role.getCode());
+                if (checkRole != null) {
+                    return Result.error(400, "角色编码已存在");
+                }
+            }
+            
+            role.setUpdateTime(LocalDateTime.now());
+            boolean success = this.updateById(role);
+            return success ? Result.success("更新成功", null) : Result.error(500, "更新失败");
+            
+        } catch (Exception e) {
+            log.error("更新角色失败", e);
+            return Result.error(500, "更新失败: " + e.getMessage());
+        }
+    }
+    
+    @Override
+    public Result<Void> deleteRole(Long id) {
+        try {
+            // 检查角色是否存在
+            Role role = this.getById(id);
+            if (role == null) {
+                return Result.error(404, "角色不存在");
+            }
+            
+            // 检查是否有用户正在使用此角色
+            int userCount = baseMapper.countUsersByRoleId(id);
+            if (userCount > 0) {
+                return Result.error(400, "该角色下还有" + userCount + "个用户,无法删除");
+            }
+            
+            boolean success = this.removeById(id);
+            return success ? Result.success("删除成功", null) : Result.error(500, "删除失败");
+            
+        } catch (Exception e) {
+            log.error("删除角色失败", e);
+            return Result.error(500, "删除失败: " + e.getMessage());
+        }
+    }
+}

BIN
menu_after_click.png


BIN
menu_after_hover.png


BIN
menu_api_response.png


BIN
menu_current_state.png


BIN
menu_diagnostic_report.png


BIN
menu_expanded.png


BIN
menu_full_page.png


+ 1 - 0
pom.xml

@@ -17,6 +17,7 @@
         <module>haha-mapper</module>
         <module>haha-service</module>
         <module>haha-miniapp</module>
+        <module>haha-admin</module>
         <module>haha-sdk</module>
     </modules>
 

BIN
sidebar_detail.png


BIN
sidebar_full_check.png


BIN
sidebar_narrow.png