瀏覽代碼

refactor: 实体模型更新及后端服务增强

- 实体: User/AssessmentOrder/Guide/GuideApplication 字段和关系更新

- 测评订单和预约: 新增字段和业务逻辑优化

- 成长档案: 服务逻辑完善

- 认证和家庭绑定: 流程优化

- DatabaseInitializer: 数据库初始化脚本同步

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 4 月之前
父節點
當前提交
2233285488

+ 112 - 1
zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java

@@ -65,7 +65,7 @@ public class DatabaseInitializer implements CommandLineRunner {
             "openid VARCHAR(64) NOT NULL, " +
             "unionid VARCHAR(64), " +
             "family_id BIGINT NOT NULL, " +
-            "role ENUM('parent','child') NOT NULL, " +
+            "role ENUM('parent','child','admin') NOT NULL, " +
             "nickname VARCHAR(50), " +
             "avatar VARCHAR(255), " +
             "password VARCHAR(128), " +
@@ -575,6 +575,24 @@ public class DatabaseInitializer implements CommandLineRunner {
             // 修改失败,忽略错误
         }
 
+        // 迁移5.1: 修改users.role为支持admin
+        try {
+            jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role ENUM('parent', 'child', 'teacher', 'admin') NOT NULL");
+            log.info("已更新users.role字段支持admin角色");
+        } catch (Exception e) {
+            // 修改失败,忽略错误
+        }
+
+        // 迁移5.2: 确保管理员角色为admin(修复旧数据遗留问题)
+        try {
+            int updated = jdbcTemplate.update("UPDATE admins SET role = 'admin' WHERE username = 'admin'");
+            if (updated > 0) {
+                log.info("已修正管理员角色为admin");
+            }
+        } catch (Exception e) {
+            // 更新失败,忽略错误
+        }
+
         // 迁移6: 创建streets表(街道地址维度表)
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS streets (" +
@@ -1021,6 +1039,56 @@ log.info("已添加template_id列到tasks表");
         } catch (Exception e) {
         }
 
+        // 迁移26: guide_applications表添加address和bank_card字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN address VARCHAR(500) COMMENT '地址'");
+            log.info("已添加address列到guide_applications表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN bank_card VARCHAR(50) COMMENT '银行卡号'");
+            log.info("已添加bank_card列到guide_applications表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        try {
+            jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN bank_name VARCHAR(100) COMMENT '开户行'");
+            log.info("已添加bank_name列到guide_applications表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移27: users表添加address字段(成长规划师地址)
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN address VARCHAR(500) COMMENT '地址'");
+            log.info("已添加address列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
+        // 迁移28: 创建guide_application_records表(申请过程记录)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_application_records (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "application_id BIGINT NOT NULL COMMENT '关联申请ID', " +
+                    "user_id BIGINT NOT NULL COMMENT '操作人用户ID', " +
+                    "action VARCHAR(50) NOT NULL COMMENT '操作: submit/approve/reject/withdraw/resubmit', " +
+                    "from_status VARCHAR(20) COMMENT '操作前状态', " +
+                    "to_status VARCHAR(20) NOT NULL COMMENT '操作后状态', " +
+                    "remark VARCHAR(500) COMMENT '备注/原因', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_application_id (application_id), " +
+                    "INDEX idx_user_id (user_id), " +
+                    "INDEX idx_action (action)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长规划师申请过程记录表'");
+            log.info("已创建guide_application_records表");
+        } catch (Exception e) {
+            // 表已存在,忽略错误
+        }
+
         log.info("数据库迁移完成");
     }
 
@@ -1223,6 +1291,41 @@ try {
   log.warn("初始化任务模板失败: {}", e.getMessage());
 }
 
+// 初始化 users 表测试用户
+try {
+  // 创建默认家庭(如果不存在)
+  Integer familyCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM families WHERE id = 1", Integer.class);
+  if (familyCount == null || familyCount == 0) {
+    jdbcTemplate.execute("INSERT INTO families (id, name, invite_code) VALUES (1, '默认家庭', '888888')");
+    log.info("默认家庭已创建");
+  }
+
+  // 密码加密: MD5("xzyj_salt_" + password)
+  String adminPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_salt_admin123".getBytes());
+  String teacherPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_salt_teacher123".getBytes());
+
+  // 创建管理员用户(管理员角色,密码 admin123)
+  Integer adminUserCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM users WHERE phone = '13800138000'", Integer.class);
+  if (adminUserCount == null || adminUserCount == 0) {
+    jdbcTemplate.execute("INSERT INTO users (openid, family_id, role, phone, nickname, real_name, password) VALUES " +
+      "('seed_admin_13800138000', 1, 'admin', '13800138000', '系统管理员', '系统管理员', '" + adminPassword + "')");
+    log.info("测试管理员用户已创建: 13800138000 (admin角色, 密码 admin123)");
+  }
+
+  // 创建测试教师用户
+  Integer teacherUserCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM users WHERE phone = '13800138001'", Integer.class);
+  if (teacherUserCount == null || teacherUserCount == 0) {
+    jdbcTemplate.execute("INSERT INTO users (openid, family_id, role, phone, nickname, real_name, password) VALUES " +
+      "('seed_teacher_13800138001', 1, 'teacher', '13800138001', '测试教师', '测试教师', '" + teacherPassword + "')");
+    log.info("测试教师用户已创建: 13800138001 (教师角色, 密码 teacher123)");
+  }
+} catch (Exception e) {
+  log.warn("初始化测试用户失败: {}", e.getMessage());
+}
+
         // 初始化成长规划师模块相关表
         initGuideModuleTables(jdbcTemplate);
     }
@@ -1437,6 +1540,14 @@ try {
             log.warn("创建guide_invite_codes表失败: {}", e.getMessage());
         }
 
+        // 迁移29: assessment_orders表添加appointment_id字段
+        try {
+            jdbcTemplate.execute("ALTER TABLE assessment_orders ADD COLUMN appointment_id BIGINT COMMENT '关联的预约ID'");
+            log.info("已添加appointment_id列到assessment_orders表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
+
         log.info("成长规划师模块数据库表初始化完成");
     }
 }

+ 4 - 0
zxyj-backend/src/main/java/com/zxyj/controller/assessment/AssessmentAppointmentController.java

@@ -59,6 +59,10 @@ public class AssessmentAppointmentController {
                 familyId, userId, childId, guideId, packageId,
                 guideName, packageName, totalPrice, discountAmount);
 
+        // 关联预约与订单
+        order.setAppointmentId(appointment.getId());
+        orderService.updateById(order);
+
         Map<String, Object> result = new HashMap<>();
         result.put("appointmentId", appointment.getId());
         result.put("orderNo", order.getOrderNo());

+ 34 - 0
zxyj-backend/src/main/java/com/zxyj/controller/assessment/AssessmentOrderController.java

@@ -93,4 +93,38 @@ public class AssessmentOrderController {
         result.put("status", "paid");
         return Result.success(result);
     }
+
+    @Operation(summary = "取消订单")
+    @PostMapping("/cancel")
+    public Result<Boolean> cancelOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        String orderNo = body.get("orderNo") != null ? body.get("orderNo").toString() : null;
+        if (orderNo == null) return Result.error("订单号不能为空");
+
+        AssessmentOrder order = orderService.getByOrderNo(orderNo);
+        if (order == null) return Result.error("订单不存在");
+        if (!order.getUserId().equals(userId)) return Result.error("无权操作");
+
+        boolean success = orderService.cancelOrder(orderNo);
+        if (!success) return Result.error("取消失败,仅待支付订单可取消");
+        return Result.success(true);
+    }
+
+    @Operation(summary = "退款订单")
+    @PostMapping("/refund")
+    public Result<Boolean> refundOrder(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+        String orderNo = body.get("orderNo") != null ? body.get("orderNo").toString() : null;
+        if (orderNo == null) return Result.error("订单号不能为空");
+
+        AssessmentOrder order = orderService.getByOrderNo(orderNo);
+        if (order == null) return Result.error("订单不存在");
+        if (!order.getUserId().equals(userId)) return Result.error("无权操作");
+
+        boolean success = orderService.refundOrder(orderNo);
+        if (!success) return Result.error("退款失败,仅已支付订单可退款");
+        return Result.success(true);
+    }
 }

+ 24 - 8
zxyj-backend/src/main/java/com/zxyj/controller/auth/AuthController.java

@@ -58,8 +58,12 @@ public class AuthController {
             return Result.error("验证码错误或已过期");
         }
         // 执行登录
-        LoginResultDTO result = userService.phoneLogin(dto);
-        return Result.success(result);
+        try {
+            LoginResultDTO result = userService.phoneLogin(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
     }
 
     /**
@@ -71,8 +75,12 @@ public class AuthController {
         if (dto.getCode() == null || dto.getPhoneCode() == null) {
             return Result.error("登录code和手机号code不能为空");
         }
-        LoginResultDTO result = userService.wechatPhoneLogin(dto);
-        return Result.success(result);
+        try {
+            LoginResultDTO result = userService.wechatPhoneLogin(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
     }
 
     /**
@@ -129,8 +137,12 @@ public class AuthController {
             return Result.error("身份证号格式不正确");
         }
         
-        LoginResultDTO result = userService.registerWithIdCard(dto);
-        return Result.success(result);
+        try {
+            LoginResultDTO result = userService.registerWithIdCard(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
     }
 
     /**
@@ -139,8 +151,12 @@ public class AuthController {
     @Operation(summary = "微信code登录")
     @PostMapping("/wechat-login")
     public Result<LoginResultDTO> wechatLogin(@RequestBody WechatLoginDTO dto) {
-        LoginResultDTO result = userService.wechatLogin(dto);
-        return Result.success(result);
+        try {
+            LoginResultDTO result = userService.wechatLogin(dto);
+            return Result.success(result);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
     }
 
     @Operation(summary = "获取当前用户信息")

+ 32 - 5
zxyj-backend/src/main/java/com/zxyj/controller/family/BindController.java

@@ -5,10 +5,12 @@ import com.zxyj.common.Result;
 import com.zxyj.dto.BindGuideDTO;
 import com.zxyj.entity.GuideFamily;
 import com.zxyj.service.GuideFamilyService;
+import com.zxyj.service.TeacherMessageService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -21,6 +23,9 @@ public class BindController {
     @Resource
     private GuideFamilyService guideFamilyService;
 
+    @Resource
+    private TeacherMessageService teacherMessageService;
+
     /**
      * 家长确认绑定指导师
      */
@@ -29,11 +34,13 @@ public class BindController {
     public Result<Boolean> acceptBind(@RequestAttribute("userId") Long userId,
                                         @RequestAttribute("familyId") Long familyId,
                                         @RequestBody BindGuideDTO dto) {
-        // 这里应该通过 bindToken 解析出指导师ID
-        // 简化处理: 假设 dto 中包含 guideId
-        boolean success = guideFamilyService.confirmBind(dto.getGuideId(), familyId,
-            dto.getServiceType(), dto.getServicePrice());
-        return Result.success(success);
+        try {
+            boolean success = guideFamilyService.confirmBind(dto.getGuideId(), familyId,
+                dto.getServiceType(), dto.getServicePrice());
+            return Result.success(success);
+        } catch (RuntimeException e) {
+            return Result.error(400, e.getMessage());
+        }
     }
 
     /**
@@ -57,4 +64,24 @@ public class BindController {
         
         return Result.success(result);
     }
+
+    /**
+     * 家长解除绑定规划师(会通知规划师)
+     */
+    @Operation(summary = "解除绑定规划师")
+    @PostMapping("/unbind")
+    public Result<Boolean> unbind(@RequestAttribute("userId") Long userId,
+                                   @RequestAttribute("familyId") Long familyId,
+                                   @RequestBody Map<String, Object> params) {
+        Long id = Long.valueOf(params.get("id").toString());
+        try {
+            GuideFamily gf = guideFamilyService.unbindAndGet(id, familyId);
+            // 发送解绑通知给规划师
+            teacherMessageService.sendMessage(familyId, userId, gf.getGuideId(),
+                "家长已解除与您的绑定关系");
+            return Result.success(true);
+        } catch (RuntimeException e) {
+            return Result.error(400, e.getMessage());
+        }
+    }
 }

+ 6 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GuideRegisterDTO.java

@@ -25,4 +25,10 @@ public class GuideRegisterDTO implements Serializable {
     private String phone;         // 手机号
 
     private String idCard;        // 身份证号
+
+    private String address;       // 地址(必填)
+
+    private String bankCard;      // 银行卡号(选填)
+
+    private String bankName;      // 开户行(选填)
 }

+ 2 - 0
zxyj-backend/src/main/java/com/zxyj/entity/AssessmentOrder.java

@@ -26,6 +26,8 @@ public class AssessmentOrder implements Serializable {
     private Long totalPrice;
     private Long discountAmount;
     private Long actualPrice;
+    private Long appointmentId;  // 关联的预约ID
+
     private String status; // pending/paid/cancelled/refunded
     private String payTime;
     private String payType; // wechat/alipay

+ 13 - 2
zxyj-backend/src/main/java/com/zxyj/entity/Guide.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.util.Date;
 
 @Data
@@ -17,7 +18,7 @@ public class Guide implements Serializable {
 
     private Long userId;
 
-    private String name;
+    private String nickname;
 
     private String phone;
 
@@ -25,7 +26,17 @@ public class Guide implements Serializable {
 
     private String status;
 
-    private Date certifiedAt;
+    private Integer serviceCount;
+
+    private Long packageId;
+
+    private String intro;
+
+    private String avatar;
+
+    private BigDecimal commissionRate;
+
+    private Date approvedAt;
 
     private Date createdAt;
 

+ 6 - 0
zxyj-backend/src/main/java/com/zxyj/entity/GuideApplication.java

@@ -34,6 +34,12 @@ public class GuideApplication implements Serializable {
 
     private String rejectReason;
 
+    private String address;
+
+    private String bankCard;
+
+    private String bankName;
+
     private Date reviewedAt;
 
     private Long reviewedBy;

+ 3 - 0
zxyj-backend/src/main/java/com/zxyj/entity/User.java

@@ -64,6 +64,9 @@ public class User implements Serializable {
     // 成长规划师审核拒绝原因
     private String teacherRejectReason;
 
+    // 地址
+    private String address;
+
     private Date createdAt;
 
     private Date updatedAt;

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/service/AssessmentAppointmentService.java

@@ -78,8 +78,20 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
     }
 
     public void confirmAppointmentByOrder(AssessmentOrder order) {
+        // 优先通过 appointmentId 直接关联
+        if (order.getAppointmentId() != null) {
+            AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(order.getAppointmentId());
+            if (appointment != null && appointment.getStatus() == 0) {
+                appointment.setStatus(1);
+                appointment.setUpdatedAt(new Date());
+                assessmentAppointmentMapper.updateById(appointment);
+                return;
+            }
+        }
+        // 回退匹配:通过 userId + childId + guideId + packageId 精确定位
         LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentAppointment::getUserId, order.getUserId());
+        wrapper.eq(AssessmentAppointment::getChildId, order.getChildId());
         wrapper.eq(AssessmentAppointment::getGuideId, order.getGuideId());
         wrapper.eq(AssessmentAppointment::getPackageId, order.getPackageId());
         wrapper.eq(AssessmentAppointment::getStatus, 0);

+ 12 - 1
zxyj-backend/src/main/java/com/zxyj/service/AssessmentOrderService.java

@@ -65,12 +65,23 @@ public class AssessmentOrderService extends ServiceImpl<AssessmentOrderMapper, A
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
         AssessmentOrder order = this.getOne(wrapper);
         if (order == null) return false;
-        if ("paid".equals(order.getStatus())) return false;
+        if (!"pending".equals(order.getStatus())) return false;
         order.setStatus("cancelled");
         order.setUpdatedAt(new Date());
         return this.updateById(order);
     }
 
+    public boolean refundOrder(String orderNo) {
+        LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AssessmentOrder::getOrderNo, orderNo);
+        AssessmentOrder order = this.getOne(wrapper);
+        if (order == null) return false;
+        if (!"paid".equals(order.getStatus())) return false;
+        order.setStatus("refunded");
+        order.setUpdatedAt(new Date());
+        return this.updateById(order);
+    }
+
     public AssessmentOrder getByOrderNo(String orderNo) {
         LambdaQueryWrapper<AssessmentOrder> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentOrder::getOrderNo, orderNo);

+ 49 - 2
zxyj-backend/src/main/java/com/zxyj/service/GrowthPlanService.java

@@ -1,13 +1,13 @@
 package com.zxyj.service;
 
-import com.zxyj.service.api.GrowthPlanServiceInterface;
-
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zxyj.entity.GrowthPlan;
 import com.zxyj.mapper.GrowthPlanMapper;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.util.Date;
+import java.util.List;
 
 @Service
 public class GrowthPlanService {
@@ -16,9 +16,56 @@ public class GrowthPlanService {
     private GrowthPlanMapper growthPlanMapper;
 
     public GrowthPlan createPlan(GrowthPlan plan) {
+        if (plan.getStatus() == null) {
+            plan.setStatus("active");
+        }
+        plan.setReviewCount(plan.getReviewCount() != null ? plan.getReviewCount() : 0);
         plan.setCreatedAt(new Date());
         plan.setUpdatedAt(new Date());
         growthPlanMapper.insert(plan);
         return plan;
     }
+
+    public GrowthPlan getById(Long id) {
+        return growthPlanMapper.selectById(id);
+    }
+
+    public List<GrowthPlan> getByChildId(Long childId) {
+        LambdaQueryWrapper<GrowthPlan> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GrowthPlan::getChildId, childId);
+        wrapper.orderByDesc(GrowthPlan::getCreatedAt);
+        return growthPlanMapper.selectList(wrapper);
+    }
+
+    public List<GrowthPlan> getByTeacherId(Long teacherId) {
+        LambdaQueryWrapper<GrowthPlan> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GrowthPlan::getTeacherId, teacherId);
+        wrapper.orderByDesc(GrowthPlan::getCreatedAt);
+        return growthPlanMapper.selectList(wrapper);
+    }
+
+    public GrowthPlan getActivePlanByChildId(Long childId) {
+        LambdaQueryWrapper<GrowthPlan> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GrowthPlan::getChildId, childId);
+        wrapper.eq(GrowthPlan::getStatus, "active");
+        wrapper.orderByDesc(GrowthPlan::getCreatedAt);
+        wrapper.last("LIMIT 1");
+        List<GrowthPlan> list = growthPlanMapper.selectList(wrapper);
+        return list.isEmpty() ? null : list.get(0);
+    }
+
+    public boolean updatePlan(GrowthPlan plan) {
+        GrowthPlan existing = growthPlanMapper.selectById(plan.getId());
+        if (existing == null) return false;
+        plan.setUpdatedAt(new Date());
+        return growthPlanMapper.updateById(plan) > 0;
+    }
+
+    public boolean reviewPlan(Long id) {
+        GrowthPlan plan = growthPlanMapper.selectById(id);
+        if (plan == null) return false;
+        plan.setReviewCount(plan.getReviewCount() != null ? plan.getReviewCount() + 1 : 1);
+        plan.setUpdatedAt(new Date());
+        return growthPlanMapper.updateById(plan) > 0;
+    }
 }