Просмотр исходного кода

feat: 添加用户角色选择、生日性别字段及家长孩子身份切换功能

- User/Child实体新增birthday和gender字段
- 登录接口支持选择角色(parent/child)并收集生日性别
- 新增长按孩子卡片切换到孩子身份功能
- 家长端可添加孩子、切换模式
- 孩子端可切换回家长模式
- 添加数据库自动同步服务
User 7 месяцев назад
Родитель
Сommit
2e14f69463

+ 59 - 3
zxyj-backend/src/main/java/com/zxyj/controller/UserController.java

@@ -4,16 +4,20 @@ import com.zxyj.common.Result;
 import com.zxyj.dto.*;
 import com.zxyj.entity.User;
 import com.zxyj.service.UserService;
-import org.springframework.beans.factory.annotation.Autowired;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.annotation.Resource;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Map;
 
+@Tag(name = "用户管理", description = "用户信息、孩子管理等接口")
 @RestController
 @RequestMapping("/api/user")
 public class UserController {
 
-    @Autowired
+    @Resource
     private UserService userService;
 
     private Long getUserId(javax.servlet.http.HttpServletRequest request) {
@@ -30,6 +34,7 @@ public class UserController {
         return null;
     }
 
+    @Operation(summary = "获取用户信息")
     @GetMapping("/info")
     public Result<User> getUserInfo(javax.servlet.http.HttpServletRequest request) {
         Long userId = getUserId(request);
@@ -37,15 +42,64 @@ public class UserController {
         return Result.success(user);
     }
 
+    @Operation(summary = "更新用户信息")
+    @PostMapping("/update")
+    public Result<Boolean> updateUserInfo(javax.servlet.http.HttpServletRequest request,
+                                          @RequestBody UpdateUserDTO dto) {
+        Long userId = getUserId(request);
+        boolean success = userService.updateUserInfo(userId, dto);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "加入家庭(通过邀请码)")
+    @PostMapping("/join-family")
+    public Result<Boolean> joinFamily(javax.servlet.http.HttpServletRequest request,
+                                      @RequestBody Map<String, String> body) {
+        Long userId = getUserId(request);
+        String inviteCode = body.get("inviteCode");
+        boolean success = userService.joinFamily(userId, inviteCode);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "创建新家庭")
+    @PostMapping("/create-family")
+    public Result<Long> createFamily(javax.servlet.http.HttpServletRequest request,
+                                     @RequestBody Map<String, String> body) {
+        Long userId = getUserId(request);
+        String familyName = body.getOrDefault("name", "我的家庭");
+        Long familyId = userService.createFamilyForUser(userId, familyName);
+        return Result.success(familyId);
+    }
+
+    @Operation(summary = "切换家长/孩子模式")
     @PostMapping("/switch-mode")
     public Result<String> switchMode(javax.servlet.http.HttpServletRequest request,
-                                     @RequestBody java.util.Map<String, String> body) {
+                                     @RequestBody Map<String, String> body) {
         Long userId = getUserId(request);
         String mode = body.get("mode");
         String result = userService.switchMode(userId, mode);
         return Result.success(result);
     }
 
+    @Operation(summary = "家长切换到指定孩子身份")
+    @PostMapping("/switch-to-child")
+    public Result<Long> switchToChild(javax.servlet.http.HttpServletRequest request,
+                                       @RequestBody Map<String, Long> body) {
+        Long userId = getUserId(request);
+        Long childId = body.get("childId");
+        Long result = userService.switchToChild(userId, childId);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "孩子切换回家长身份")
+    @PostMapping("/switch-back-to-parent")
+    public Result<Boolean> switchBackToParent(javax.servlet.http.HttpServletRequest request) {
+        Long userId = getUserId(request);
+        boolean result = userService.switchBackToParent(userId);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "创建孩子")
     @PostMapping("/children")
     public Result<ChildInfoDTO> createChild(javax.servlet.http.HttpServletRequest request,
                                             @RequestBody CreateChildDTO dto) {
@@ -54,6 +108,7 @@ public class UserController {
         return Result.success(child);
     }
 
+    @Operation(summary = "获取孩子列表")
     @GetMapping("/children")
     public Result<List<ChildInfoDTO>> getChildren(javax.servlet.http.HttpServletRequest request) {
         Long userId = getUserId(request);
@@ -61,6 +116,7 @@ public class UserController {
         return Result.success(children);
     }
 
+    @Operation(summary = "更新孩子信息")
     @PutMapping("/children/{childId}")
     public Result<Boolean> updateChild(javax.servlet.http.HttpServletRequest request,
                                        @PathVariable Long childId,

+ 3 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ChildInfoDTO.java

@@ -1,12 +1,15 @@
 package com.zxyj.dto;
 
 import lombok.Data;
+import java.util.Date;
 
 @Data
 public class ChildInfoDTO {
     private Long id;
     private String nickname;
     private Integer age;
+    private Date birthday;
+    private String gender;
     private String danLevel;
     private Integer penaltyEnabled;
     private String theme;

+ 3 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateChildDTO.java

@@ -1,10 +1,13 @@
 package com.zxyj.dto;
 
 import lombok.Data;
+import java.util.Date;
 
 @Data
 public class CreateChildDTO {
     private String nickname;
     private Integer age;
+    private Date birthday;
+    private String gender;
     private String danLevel;
 }

+ 2 - 0
zxyj-backend/src/main/java/com/zxyj/dto/LoginResultDTO.java

@@ -9,4 +9,6 @@ public class LoginResultDTO {
     private String role;
     private Long familyId;
     private String nickname;
+    private Long currentChildId; // 当前切换到的孩子ID(如果切换了)
+    private Boolean isNewUser; // 是否新用户,用于前端判断是否需要进入信息编辑页面
 }

+ 4 - 0
zxyj-backend/src/main/java/com/zxyj/dto/PhoneLoginDTO.java

@@ -1,6 +1,7 @@
 package com.zxyj.dto;
 
 import lombok.Data;
+import java.util.Date;
 
 @Data
 public class PhoneLoginDTO {
@@ -8,4 +9,7 @@ public class PhoneLoginDTO {
     private String code;
     private String nickname;
     private String avatar;
+    private String role; // 角色: parent/child
+    private Date birthday;
+    private String gender;
 }

+ 4 - 0
zxyj-backend/src/main/java/com/zxyj/entity/Child.java

@@ -23,6 +23,10 @@ public class Child implements Serializable {
 
     private Integer age;
 
+    private Date birthday; // 生日
+
+    private String gender; // 性别: male/female
+
     private String danLevel;
 
     private Integer penaltyEnabled;

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

@@ -29,6 +29,16 @@ public class User implements Serializable {
 
     private String password;
 
+    private String idCard; // 身份证号
+
+    private String realName; // 真实姓名
+
+    private String phone; // 手机号
+
+    private Date birthday; // 生日
+
+    private String gender; // 性别: male/female
+
     private Date createdAt;
 
     private Date updatedAt;

+ 201 - 0
zxyj-backend/src/main/java/com/zxyj/service/DatabaseSyncService.java

@@ -0,0 +1,201 @@
+package com.zxyj.service;
+
+import com.zxyj.entity.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 数据库表结构自动同步服务
+ * 当Entity类新增字段时,自动在数据库中添加对应的列
+ */
+@Slf4j
+@Service
+public class DatabaseSyncService {
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    /**
+     * Entity类与表名的映射
+     */
+    private static final Map<Class<?>, String> ENTITY_TABLE_MAP = new HashMap<>();
+
+    static {
+        ENTITY_TABLE_MAP.put(User.class, "users");
+        ENTITY_TABLE_MAP.put(Child.class, "children");
+        ENTITY_TABLE_MAP.put(Family.class, "families");
+        ENTITY_TABLE_MAP.put(Task.class, "tasks");
+        ENTITY_TABLE_MAP.put(Reward.class, "rewards");
+        ENTITY_TABLE_MAP.put(PointsLog.class, "points_log");
+        ENTITY_TABLE_MAP.put(VerificationCode.class, "verification_codes");
+    }
+
+    /**
+     * 实体类字段与数据库类型的映射
+     */
+    private static final Map<Class<?>, String> JAVA_TYPE_TO_SQL_TYPE = new HashMap<>();
+
+    static {
+        JAVA_TYPE_TO_SQL_TYPE.put(String.class, "VARCHAR(255)");
+        JAVA_TYPE_TO_SQL_TYPE.put(Long.class, "BIGINT");
+        JAVA_TYPE_TO_SQL_TYPE.put(Integer.class, "INT");
+        JAVA_TYPE_TO_SQL_TYPE.put(Boolean.class, "TINYINT(1)");
+        JAVA_TYPE_TO_SQL_TYPE.put(Date.class, "DATETIME");
+        JAVA_TYPE_TO_SQL_TYPE.put(java.sql.Date.class, "DATE");
+        JAVA_TYPE_TO_SQL_TYPE.put(java.sql.Timestamp.class, "TIMESTAMP");
+        JAVA_TYPE_TO_SQL_TYPE.put(Byte.class, "TINYINT");
+        JAVA_TYPE_TO_SQL_TYPE.put(Short.class, "SMALLINT");
+        JAVA_TYPE_TO_SQL_TYPE.put(Float.class, "FLOAT");
+        JAVA_TYPE_TO_SQL_TYPE.put(Double.class, "DOUBLE");
+        JAVA_TYPE_TO_SQL_TYPE.put(java.math.BigDecimal.class, "DECIMAL(10,2)");
+    }
+
+    @PostConstruct
+    public void syncDatabaseSchema() {
+        log.info("开始同步数据库表结构...");
+        long startTime = System.currentTimeMillis();
+
+        for (Map.Entry<Class<?>, String> entry : ENTITY_TABLE_MAP.entrySet()) {
+            try {
+                syncEntityToTable(entry.getKey(), entry.getValue());
+            } catch (Exception e) {
+                log.error("同步表 {} 失败: {}", entry.getValue(), e.getMessage());
+            }
+        }
+
+        long endTime = System.currentTimeMillis();
+        log.info("数据库表结构同步完成,耗时: {} ms", endTime - startTime);
+    }
+
+    /**
+     * 同步Entity到数据库表
+     */
+    private void syncEntityToTable(Class<?> entityClass, String tableName) {
+        // 检查表是否存在
+        if (!isTableExists(tableName)) {
+            log.warn("表 {} 不存在,跳过同步", tableName);
+            return;
+        }
+
+        // 获取数据库表的列信息
+        Set<String> dbColumns = getTableColumns(tableName);
+
+        // 获取Entity类的字段
+        Map<String, Class<?>> entityFields = getEntityFields(entityClass);
+
+        // 比较并添加缺失的列
+        for (Map.Entry<String, Class<?>> fieldEntry : entityFields.entrySet()) {
+            String columnName = fieldEntry.getKey();
+            Class<?> fieldType = fieldEntry.getValue();
+
+            if (!dbColumns.contains(columnName.toLowerCase())) {
+                addColumn(tableName, columnName, fieldType);
+            }
+        }
+    }
+
+    /**
+     * 检查表是否存在
+     */
+    private boolean isTableExists(String tableName) {
+        try {
+            String sql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?";
+            Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName);
+            return count != null && count > 0;
+        } catch (Exception e) {
+            log.error("检查表 {} 是否存在失败: {}", tableName, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 获取表的列名集合
+     */
+    private Set<String> getTableColumns(String tableName) {
+        try {
+            String sql = "SELECT COLUMN_NAME FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ?";
+            List<String> columns = jdbcTemplate.queryForList(sql, String.class, tableName);
+            return new HashSet<>(columns.stream().map(String::toLowerCase).collect(Collectors.toList()));
+        } catch (Exception e) {
+            log.error("获取表 {} 列信息失败: {}", tableName, e.getMessage());
+            return new HashSet<>();
+        }
+    }
+
+    /**
+     * 获取Entity类的字段(排除ID、创建时间、更新时间)
+     */
+    private Map<String, Class<?>> getEntityFields(Class<?> entityClass) {
+        Map<String, Class<?>> fields = new LinkedHashMap<>();
+        java.lang.reflect.Field[] declaredFields = entityClass.getDeclaredFields();
+
+        for (java.lang.reflect.Field field : declaredFields) {
+            String fieldName = field.getName();
+
+            // 排除 serialVersionUID
+            if ("serialVersionUID".equals(fieldName)) {
+                continue;
+            }
+
+            // 排除静态字段
+            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
+                continue;
+            }
+
+            // 转换为数据库列名(下划线命名)
+            String columnName = camelToSnake(fieldName);
+            fields.put(columnName, field.getType());
+        }
+
+        return fields;
+    }
+
+    /**
+     * 驼峰命名转下划线命名
+     */
+    private String camelToSnake(String camelCase) {
+        if (camelCase == null || camelCase.isEmpty()) {
+            return camelCase;
+        }
+
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < camelCase.length(); i++) {
+            char c = camelCase.charAt(i);
+            if (Character.isUpperCase(c)) {
+                if (i > 0) {
+                    result.append('_');
+                }
+                result.append(Character.toLowerCase(c));
+            } else {
+                result.append(c);
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * 添加列
+     */
+    private void addColumn(String tableName, String columnName, Class<?> fieldType) {
+        String sqlType = JAVA_TYPE_TO_SQL_TYPE.getOrDefault(fieldType, "VARCHAR(255)");
+
+        // 特殊处理
+        if (fieldType == Date.class) {
+            sqlType = "DATETIME";
+        }
+
+        try {
+            String sql = String.format("ALTER TABLE %s ADD COLUMN %s %s", tableName, columnName, sqlType);
+            jdbcTemplate.execute(sql);
+            log.info("表 {} 新增列 {} ({})", tableName, columnName, sqlType);
+        } catch (Exception e) {
+            log.error("表 {} 新增列 {} 失败: {}", tableName, columnName, e.getMessage());
+        }
+    }
+}

+ 201 - 12
zxyj-backend/src/main/java/com/zxyj/service/UserService.java

@@ -3,13 +3,15 @@ package com.zxyj.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zxyj.config.JwtConfig;
 import com.zxyj.dto.*;
+import com.zxyj.dto.RegisterWithIdCardDTO;
 import com.zxyj.entity.Child;
 import com.zxyj.entity.Family;
 import com.zxyj.entity.User;
 import com.zxyj.mapper.ChildMapper;
 import com.zxyj.mapper.FamilyMapper;
 import com.zxyj.mapper.UserMapper;
-import org.springframework.beans.factory.annotation.Autowired;
+import com.zxyj.service.VerificationCodeService;
+import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.util.DigestUtils;
 
@@ -21,17 +23,20 @@ import java.util.UUID;
 @Service
 public class UserService {
 
-    @Autowired
-    private UserMapper userMapper;
+@Resource
+private UserMapper userMapper;
 
-    @Autowired
-    private FamilyMapper familyMapper;
+@Resource
+private FamilyMapper familyMapper;
 
-    @Autowired
-    private ChildMapper childMapper;
+@Resource
+private ChildMapper childMapper;
 
-    @Autowired
-    private JwtConfig jwtConfig;
+@Resource
+private JwtConfig jwtConfig;
+
+@Resource
+private VerificationCodeService verificationCodeService;
 
     public LoginResultDTO wechatLogin(WechatLoginDTO dto) {
         // 模拟微信登录,实际需要调用微信API获取openid
@@ -82,13 +87,17 @@ public class UserService {
     public LoginResultDTO phoneLogin(PhoneLoginDTO dto) {
         // 使用手机号作为唯一标识
         String phone = dto.getPhone();
-        
+
         // 查询用户是否存在
         User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                 .eq(User::getOpenid, "phone:" + phone));
 
+        boolean isNewUser = false;
+
         if (user == null) {
-            // 新用户,创建家庭和用户
+            // 新用户,创建新家庭和用户
+            isNewUser = true;
+
             Family family = new Family();
             family.setName("我的家庭");
             family.setInviteCode(generateInviteCode());
@@ -100,9 +109,13 @@ public class UserService {
             user.setOpenid("phone:" + phone);
             user.setUnionid("");
             user.setFamilyId(family.getId());
-            user.setRole("parent");
+            // 默认角色为家长,可以被覆盖
+            user.setRole(dto.getRole() != null ? dto.getRole() : "parent");
             user.setNickname(dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7));
             user.setAvatar(dto.getAvatar());
+            user.setPhone(phone);
+            user.setBirthday(dto.getBirthday());
+            user.setGender(dto.getGender());
             user.setPassword("");
             user.setCreatedAt(new Date());
             user.setUpdatedAt(new Date());
@@ -118,6 +131,7 @@ public class UserService {
         result.setRole(user.getRole());
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
+        result.setIsNewUser(isNewUser);
 
         return result;
     }
@@ -156,6 +170,109 @@ public class UserService {
         return mode;
     }
 
+    /**
+     * 家长切换到指定孩子身份
+     */
+    public Long switchToChild(Long userId, Long childId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || !"parent".equals(user.getRole())) {
+            return null;
+        }
+        
+        Child child = childMapper.selectById(childId);
+        if (child == null || !child.getFamilyId().equals(user.getFamilyId())) {
+            return null;
+        }
+        
+        // 保存原用户ID到会话中,后续可以切回
+        // 这里用简单的机制:存储到用户的 extended 字段或者新表
+        // 为简化,先返回 childId,实际切换通过 setRole 实现
+        
+        // 实际切换角色为 child
+        user.setRole("child");
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        
+        return childId;
+    }
+
+    /**
+     * 孩子切换回家长身份
+     */
+    public boolean switchBackToParent(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null || !"child".equals(user.getRole())) {
+            return false;
+        }
+        user.setRole("parent");
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return true;
+    }
+
+    public LoginResultDTO registerWithIdCard(RegisterWithIdCardDTO dto) {
+        // 验证验证码
+        boolean valid = verificationCodeService.verifyCode(dto.getPhone(), dto.getCode(), "login");
+        if (!valid) {
+            throw new RuntimeException("验证码错误或已过期");
+        }
+
+        // 通过身份证查找是否已有注册的用户
+        User existingUser = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getIdCard, dto.getIdCard()));
+        
+        Family family;
+        User user;
+        
+        if (existingUser != null) {
+            // 如果存在相同身份证的用户,加入其家庭
+            user = existingUser;
+            family = familyMapper.selectById(user.getFamilyId());
+            
+            // 更新用户信息(昵称和头像可以更新)
+            user.setNickname(dto.getNickname());
+            user.setAvatar(dto.getAvatar());
+            // 手机号也可以更新(如果需要)
+            user.setOpenid("phone:" + dto.getPhone());
+            user.setUpdatedAt(new Date());
+            userMapper.updateById(user);
+        } else {
+            // 创建新家庭和用户
+            family = new Family();
+            family.setName("我的家庭");
+            family.setInviteCode(generateInviteCode());
+            family.setCreatedAt(new Date());
+            family.setUpdatedAt(new Date());
+            familyMapper.insert(family);
+
+            user = new User();
+            user.setOpenid("phone:" + dto.getPhone());
+            user.setUnionid("");
+            user.setFamilyId(family.getId());
+            user.setRole("parent");
+            user.setNickname(dto.getNickname());
+            user.setAvatar(dto.getAvatar());
+            user.setIdCard(dto.getIdCard());
+            user.setRealName(dto.getRealName());
+            user.setPassword(""); // 初始密码为空,需要后续设置
+            user.setCreatedAt(new Date());
+            user.setUpdatedAt(new Date());
+            userMapper.insert(user);
+        }
+
+        // 生成token
+        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+
+        LoginResultDTO result = new LoginResultDTO();
+        result.setToken(token);
+        result.setUserId(user.getId());
+        result.setRole(user.getRole());
+        result.setFamilyId(user.getFamilyId());
+        result.setNickname(user.getNickname());
+
+        return result;
+    }
+
     public ChildInfoDTO createChild(Long userId, CreateChildDTO dto) {
         User user = userMapper.selectById(userId);
         if (user == null) {
@@ -167,6 +284,8 @@ public class UserService {
         child.setFamilyId(user.getFamilyId());
         child.setNickname(dto.getNickname());
         child.setAge(dto.getAge());
+        child.setBirthday(dto.getBirthday());
+        child.setGender(dto.getGender());
         child.setDanLevel(dto.getDanLevel());
         // 根据年龄设置扣分开关:6岁以下默认关闭,6岁及以上默认开启
         child.setPenaltyEnabled(dto.getAge() >= 6 ? 1 : 0);
@@ -234,6 +353,8 @@ public class UserService {
         dto.setId(child.getId());
         dto.setNickname(child.getNickname());
         dto.setAge(child.getAge());
+        dto.setBirthday(child.getBirthday());
+        dto.setGender(child.getGender());
         dto.setDanLevel(child.getDanLevel());
         dto.setPenaltyEnabled(child.getPenaltyEnabled());
         dto.setTheme(child.getTheme());
@@ -242,4 +363,72 @@ public class UserService {
         dto.setFocusRemaining(child.getFocusRemaining());
         return dto;
     }
+
+    /**
+     * 更新用户信息
+     */
+    public boolean updateUserInfo(Long userId, UpdateUserDTO dto) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return false;
+        }
+        if (dto.getNickname() != null) {
+            user.setNickname(dto.getNickname());
+        }
+        if (dto.getAvatar() != null) {
+            user.setAvatar(dto.getAvatar());
+        }
+        if (dto.getRealName() != null) {
+            user.setRealName(dto.getRealName());
+        }
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return true;
+    }
+
+    /**
+     * 通过邀请码加入家庭
+     */
+    public boolean joinFamily(Long userId, String inviteCode) {
+        if (inviteCode == null || inviteCode.isEmpty()) {
+            throw new RuntimeException("邀请码不能为空");
+        }
+        // 查找家庭
+        Family family = familyMapper.selectOne(new LambdaQueryWrapper<Family>()
+                .eq(Family::getInviteCode, inviteCode));
+        if (family == null) {
+            throw new RuntimeException("邀请码无效");
+        }
+        // 更新用户的家庭ID
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        user.setFamilyId(family.getId());
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return true;
+    }
+
+    /**
+     * 为用户创建新家庭
+     */
+    public Long createFamilyForUser(Long userId, String familyName) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        // 创建新家庭
+        Family family = new Family();
+        family.setName(familyName);
+        family.setInviteCode(generateInviteCode());
+        family.setCreatedAt(new Date());
+        family.setUpdatedAt(new Date());
+        familyMapper.insert(family);
+        // 更新用户的家庭ID
+        user.setFamilyId(family.getId());
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return family.getId();
+    }
 }

+ 40 - 13
zxyj-backend/src/main/resources/schema.sql

@@ -21,10 +21,16 @@ CREATE TABLE IF NOT EXISTS users (
     nickname VARCHAR(50),
     avatar VARCHAR(255),
     password VARCHAR(128),
+    id_card VARCHAR(18),
+    real_name VARCHAR(50),
+    phone VARCHAR(11),
+    birthday DATE,
+    gender ENUM('male','female'),
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_openid (openid),
-    INDEX idx_family_id (family_id)
+    INDEX idx_family_id (family_id),
+    INDEX idx_phone (phone)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- 孩子详情表
@@ -34,6 +40,8 @@ CREATE TABLE IF NOT EXISTS children (
     family_id BIGINT NOT NULL,
     nickname VARCHAR(50) NOT NULL,
     age TINYINT NOT NULL,
+    birthday DATE,
+    gender ENUM('male','female'),
     dan_level VARCHAR(2),
     penalty_enabled TINYINT DEFAULT 1 COMMENT '是否开启扣分(0否1是)',
     theme VARCHAR(32) DEFAULT 'default',
@@ -42,7 +50,7 @@ CREATE TABLE IF NOT EXISTS children (
     last_task_date DATE,
     focus_max_daily TINYINT DEFAULT 3,
     focus_remaining TINYINT DEFAULT 3,
-    focus_reset_date DATE DEFAULT (CURRENT_DATE),
+    focus_reset_date DATE,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_user_id (user_id),
@@ -244,15 +252,34 @@ CREATE TABLE IF NOT EXISTS dan_assessments (
 
 -- 任务模板表
 CREATE TABLE IF NOT EXISTS task_templates (
-    id BIGINT AUTO_INCREMENT PRIMARY KEY,
-    dan_level VARCHAR(2) NOT NULL,
-    title VARCHAR(200) NOT NULL,
-    points TINYINT DEFAULT 2,
-    duration_minutes TINYINT,
-    category VARCHAR(32),
-    is_default TINYINT DEFAULT 1,
-    family_id BIGINT,
-    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_dan_level (dan_level),
-    INDEX idx_is_default (is_default)
+id BIGINT AUTO_INCREMENT PRIMARY KEY,
+dan_level VARCHAR(2) NOT NULL,
+title VARCHAR(200) NOT NULL,
+points TINYINT DEFAULT 2,
+duration_minutes TINYINT,
+category VARCHAR(32),
+is_default TINYINT DEFAULT 1,
+family_id BIGINT,
+created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+INDEX idx_dan_level (dan_level),
+INDEX idx_is_default (is_default)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 媒体记录表(支持图片、视频、音频、文字)
+CREATE TABLE IF NOT EXISTS media_records (
+id BIGINT AUTO_INCREMENT PRIMARY KEY,
+type ENUM('image', 'video', 'audio', 'text') NOT NULL,
+url VARCHAR(500),
+description TEXT,
+task_id BIGINT NOT NULL,
+creator_id BIGINT NOT NULL,
+original_filename VARCHAR(255),
+file_size BIGINT,
+content_type VARCHAR(100),
+created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+INDEX idx_task_id (task_id),
+INDEX idx_creator_id (creator_id),
+INDEX idx_type (type),
+FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
+FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

+ 15 - 0
zxyj-frontend/index.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
+    <title>心知家庭</title>
+    <!--preload-links-->
+    <!--app-context-->
+    <link rel="stylesheet" href="/static/index.css" />
+</head>
+<body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+</body>
+</html>

+ 67 - 0
zxyj-frontend/manifest.json

@@ -0,0 +1,67 @@
+{
+    "name" : "心知家庭",
+    "appid" : "__UNI__DA59D6D",
+    "description" : "家庭教育任务管理小程序",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        "modules" : {},
+        "distribute" : {
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            "ios" : {},
+            "sdkConfigs" : {}
+        }
+    },
+    "quickapp" : {},
+    "mp-weixin" : {
+        "appid" : "wx5ba8038ef16fb245",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : true,
+            "postcss" : true,
+            "minified" : true
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "2"
+}

+ 22 - 12
zxyj-frontend/pages.json

@@ -7,6 +7,24 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/user-edit/user-edit",
+      "style": {
+        "navigationBarTitleText": "完善信息"
+      }
+    },
+    {
+      "path": "pages/index/child-index",
+      "style": {
+        "navigationBarTitleText": "首页"
+      }
+    },
+    {
+      "path": "pages/index/parent-index",
+      "style": {
+        "navigationBarTitleText": "首页"
+      }
+    },
     {
       "path": "pages/index/index",
       "style": {
@@ -46,27 +64,19 @@
     "list": [
       {
         "pagePath": "pages/index/index",
-        "text": "首页",
-        "iconPath": "static/tab-home.png",
-        "selectedIconPath": "static/tab-home-active.png"
+        "text": "首页"
       },
       {
         "pagePath": "pages/tasks/tasks",
-        "text": "任务",
-        "iconPath": "static/tab-task.png",
-        "selectedIconPath": "static/tab-task-active.png"
+        "text": "任务"
       },
       {
         "pagePath": "pages/rewards/rewards",
-        "text": "心愿单",
-        "iconPath": "static/tab-reward.png",
-        "selectedIconPath": "static/tab-reward-active.png"
+        "text": "心愿单"
       },
       {
         "pagePath": "pages/profile/profile",
-        "text": "我的",
-        "iconPath": "static/tab-profile.png",
-        "selectedIconPath": "static/tab-profile-active.png"
+        "text": "我的"
       }
     ]
   }

+ 326 - 0
zxyj-frontend/pages/index/child-index.vue

@@ -0,0 +1,326 @@
+<template>
+  <view class="container">
+    <!-- 欢迎区域 -->
+    <view class="welcome-section">
+      <view class="welcome-text">
+        <text class="hello">你好,{{ nickname || '小朋友' }}</text>
+        <text class="subtitle">今天也要加油完成任务哦!</text>
+      </view>
+      <view class="streak-badge" v-if="streakDays > 0">
+        <text class="fire">🔥</text>
+        <text class="days">{{ streakDays }}天</text>
+      </view>
+    </view>
+
+    <!-- 积分卡片 -->
+    <view class="points-card">
+      <view class="points-label">当前积分</view>
+      <view class="points-value">{{ totalPoints }}</view>
+      <view class="points-expire" v-if="expireDate">有效期至 {{ expireDate }}</view>
+    </view>
+
+    <!-- 今日任务概览 -->
+    <view class="section">
+      <view class="section-title">今日任务</view>
+      <view class="task-summary">
+        <view class="summary-item">
+          <text class="num">{{ completedCount }}</text>
+          <text class="label">已完成</text>
+        </view>
+        <view class="summary-item">
+          <text class="num">{{ pendingCount }}</text>
+          <text class="label">待完成</text>
+        </view>
+        <view class="summary-item">
+          <text class="num">{{ todayPoints }}</text>
+          <text class="label">今日获得</text>
+        </view>
+      </view>
+      <button class="btn-primary view-all-btn" @click="goToTasks">查看全部任务</button>
+    </view>
+
+    <!-- 快捷操作 -->
+    <view class="section">
+      <view class="section-title">快捷操作</view>
+      <view class="quick-actions">
+        <view class="action-item" @click="goToRewards">
+          <text class="icon">🎁</text>
+          <text class="label">心愿单</text>
+        </view>
+        <view class="action-item" @click="goToFocus">
+          <text class="icon">⏱️</text>
+          <text class="label">专注训练</text>
+        </view>
+        <view class="action-item" @click="switchMode">
+          <text class="icon">🔄</text>
+          <text class="label">切换模式</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getTodayTasks, getPointsBalance, getChildren, switchBackToParent } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      nickname: '',
+      totalPoints: 0,
+      todayPoints: 0,
+      streakDays: 0,
+      expireDate: '',
+      completedCount: 0,
+      pendingCount: 0,
+      currentChildId: ''
+    }
+  },
+  onLoad() {
+    this.checkLogin()
+  },
+  onShow() {
+    if (uni.getStorageSync('token')) {
+      this.loadData()
+    }
+  },
+  methods: {
+    checkLogin() {
+      if (!uni.getStorageSync('token')) {
+        uni.reLaunch({
+          url: '/pages/login/login'
+        })
+      }
+    },
+    async loadData() {
+      try {
+        // 获取孩子列表
+        const childrenRes = await getChildren()
+        const children = childrenRes.data
+        if (children && children.length > 0) {
+          const child = children[0]
+          this.currentChildId = child.id
+          this.nickname = child.nickname
+          this.totalPoints = child.totalPoints
+          this.streakDays = child.streakDays
+          
+          // 获取积分余额
+          const balanceRes = await getPointsBalance(child.id)
+          this.totalPoints = balanceRes.data.balance
+          this.expireDate = this.formatDate(balanceRes.data.expireDate)
+          
+          // 获取今日任务
+          const tasksRes = await getTodayTasks(child.id)
+          const tasks = tasksRes.data
+          this.completedCount = tasks.filter(t => t.status === 'completed').length
+          this.pendingCount = tasks.filter(t => t.status === 'pending').length
+          
+          // 计算今日获得积分
+          this.todayPoints = tasks
+            .filter(t => t.status === 'completed')
+            .reduce((sum, t) => sum + (t.points || 0), 0)
+        }
+      } catch (e) {
+        console.error('加载数据失败', e)
+      }
+    },
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      const date = new Date(dateStr)
+      return `${date.getMonth() + 1}月${date.getDate()}日`
+    },
+    goToTasks() {
+      uni.switchTab({
+        url: '/pages/tasks/tasks'
+      })
+    },
+    goToRewards() {
+      uni.switchTab({
+        url: '/pages/rewards/rewards'
+      })
+    },
+    goToFocus() {
+      uni.navigateTo({
+        url: '/pages/focus/focus'
+      })
+    },
+    switchMode() {
+      // 孩子端点击切换模式时,直接切换回家长模式
+      uni.showModal({
+        title: '切换模式',
+        content: '确定要切换回家长模式吗?',
+        success: async (res) => {
+          if (res.confirm) {
+            await this.switchBackToParentMode()
+          }
+        }
+      })
+    },
+    // 切换回家长模式
+    async switchBackToParentMode() {
+      try {
+        const result = await switchBackToParent()
+        if (result.data) {
+          this.$store.commit('switchRole', 'parent')
+          uni.showToast({
+            title: '已切换回家长模式',
+            icon: 'success'
+          })
+          // 跳转到家长首页
+          uni.reLaunch({
+            url: '/pages/index/parent-index'
+          })
+        }
+      } catch (e) {
+        uni.showToast({
+          title: '切换失败',
+          icon: 'none'
+        })
+      }
+    },
+    navigateToHome(role) {
+      if (role === 'parent') {
+        uni.reLaunch({
+          url: '/pages/index/parent-index'
+        })
+      } else {
+        uni.reLaunch({
+          url: '/pages/index/child-index'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+}
+
+.welcome-section {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+
+.hello {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.subtitle {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.streak-badge {
+  display: flex;
+  align-items: center;
+  background: linear-gradient(135deg, #FF6B6B, #FF8E53);
+  padding: 10rpx 20rpx;
+  border-radius: 30rpx;
+}
+
+.fire {
+  font-size: 32rpx;
+}
+
+.days {
+  color: #fff;
+  font-size: 24rpx;
+  margin-left: 8rpx;
+}
+
+.points-card {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 20rpx;
+  padding: 40rpx;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+
+.points-label {
+  color: rgba(255, 255, 255, 0.8);
+  font-size: 26rpx;
+}
+
+.points-value {
+  color: #fff;
+  font-size: 72rpx;
+  font-weight: bold;
+  margin: 20rpx 0;
+}
+
+.points-expire {
+  color: rgba(255, 255, 255, 0.6);
+  font-size: 22rpx;
+}
+
+.section {
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+
+.task-summary {
+  display: flex;
+  justify-content: space-around;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+}
+
+.summary-item {
+  text-align: center;
+}
+
+.summary-item .num {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #FF6B6B;
+  display: block;
+}
+
+.summary-item .label {
+  font-size: 24rpx;
+  color: #666;
+}
+
+.view-all-btn {
+  width: 100%;
+}
+
+.quick-actions {
+  display: flex;
+  justify-content: space-around;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+
+.action-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.action-item .icon {
+  font-size: 48rpx;
+  margin-bottom: 10rpx;
+}
+
+.action-item .label {
+  font-size: 24rpx;
+  color: #666;
+}
+</style>

+ 470 - 0
zxyj-frontend/pages/index/parent-index.vue

@@ -0,0 +1,470 @@
+<template>
+  <view class="container">
+    <!-- 欢迎区域 -->
+    <view class="welcome-section">
+      <view class="welcome-text">
+        <text class="hello">家长您好</text>
+        <text class="subtitle">管理家庭任务,了解孩子动态</text>
+      </view>
+    </view>
+
+    <!-- 家庭孩子列表 -->
+    <view class="section">
+      <view class="section-title">家庭成员</view>
+      <view class="children-list">
+        <view 
+          class="child-card" 
+          v-for="child in children" 
+          :key="child.id"
+          @click="selectChild(child)"
+          @longpress="switchToChildMode(child)"
+        >
+          <view class="child-avatar">{{ child.nickname ? child.nickname[0] : '?' }}</view>
+          <view class="child-info">
+            <text class="child-name">{{ child.nickname || '孩子' }}</text>
+            <text class="child-points">积分: {{ child.totalPoints }}</text>
+          </view>
+          <view class="child-streak" v-if="child.streakDays > 0">
+            <text class="fire">🔥</text>
+            <text class="days">{{ child.streakDays }}天</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 待审核任务 -->
+    <view class="section" v-if="pendingReviewTasks.length > 0">
+      <view class="section-title">待审核任务</view>
+      <view class="review-list">
+        <view 
+          class="review-item" 
+          v-for="task in pendingReviewTasks" 
+          :key="task.id"
+        >
+          <view class="review-info">
+            <text class="task-name">{{ task.title }}</text>
+            <text class="task-child">提供者: {{ task.childName }}</text>
+          </view>
+          <view class="review-actions">
+            <button class="btn-pass" @click="approveTask(task.id)">通过</button>
+            <button class="btn-reject" @click="rejectTask(task.id)">拒绝</button>
+          </view>
+        </view>
+        <button class="btn-primary view-all-btn" @click="goToReview">查看全部审核</button>
+      </view>
+    </view>
+
+    <!-- 快捷操作 -->
+    <view class="section">
+      <view class="section-title">快捷操作</view>
+      <view class="quick-actions">
+        <view class="action-item" @click="addTask">
+          <text class="icon">➕</text>
+          <text class="label">添加任务</text>
+        </view>
+        <view class="action-item" @click="goToReview">
+          <text class="icon">📋</text>
+          <text class="label">审核任务</text>
+        </view>
+        <view class="action-item" @click="addChild">
+          <text class="icon">👶</text>
+          <text class="label">添加孩子</text>
+        </view>
+        <view class="action-item" @click="switchMode">
+          <text class="icon">🔄</text>
+          <text class="label">切换模式</text>
+        </view>
+      </view>
+    </view>
+        <view class="action-item" @click="goToReview">
+          <text class="icon">📋</text>
+          <text class="label">审核任务</text>
+        </view>
+        <view class="action-item" @click="manageChildren">
+          <text class="icon">👨‍👩‍👧</text>
+          <text class="label">孩子管理</text>
+        </view>
+        <view class="action-item" @click="switchMode">
+          <text class="icon">🔄</text>
+          <text class="label">切换模式</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getChildren, getPendingReviewTasks, approveTask, rejectTask, switchToChild, switchBackToParent } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      children: [],
+      pendingReviewTasks: [],
+      selectedChildId: '',
+      currentChildId: ''  // 当前切换到的孩子ID
+    }
+  },
+  onLoad() {
+    this.checkLogin()
+  },
+  onShow() {
+    if (uni.getStorageSync('token')) {
+      this.loadData()
+    }
+  },
+  methods: {
+    checkLogin() {
+      if (!uni.getStorageSync('token')) {
+        uni.reLaunch({
+          url: '/pages/login/login'
+        })
+      }
+    },
+    async loadData() {
+      try {
+        // 获取孩子列表
+        const childrenRes = await getChildren()
+        this.children = childrenRes.data || []
+        
+        // 获取待审核任务
+        const reviewRes = await getPendingReviewTasks()
+        this.pendingReviewTasks = (reviewRes.data || []).slice(0, 3) // 只显示前3条
+      } catch (e) {
+        console.error('加载数据失败', e)
+      }
+    },
+    selectChild(child) {
+      this.$store.commit('setCurrentChild', child.id)
+      uni.showToast({
+        title: `已选择 ${child.nickname}`,
+        icon: 'none'
+      })
+    },
+    // 切换到孩子身份
+    async switchToChildMode(child) {
+      uni.showModal({
+        title: '切换到孩子模式',
+        content: `确定要切换到 ${child.nickname} 的模式吗?`,
+        success: async (res) => {
+          if (res.confirm) {
+            try {
+              const result = await switchToChild(child.id)
+              if (result.data) {
+                this.currentChildId = child.id
+                this.$store.commit('switchRole', 'child')
+                this.$store.commit('setCurrentChild', child.id)
+                uni.showToast({
+                  title: `已切换到 ${child.nickname} 模式`,
+                  icon: 'success'
+                })
+                // 跳转到孩子首页
+                uni.reLaunch({
+                  url: '/pages/index/child-index'
+                })
+              }
+            } catch (e) {
+              uni.showToast({
+                title: '切换失败',
+                icon: 'none'
+              })
+            }
+          }
+        }
+      })
+    },
+    // 切换回家长模式
+    async switchBackToParentMode() {
+      try {
+        const result = await switchBackToParent()
+        if (result.data) {
+          this.currentChildId = ''
+          this.$store.commit('switchRole', 'parent')
+          uni.showToast({
+            title: '已切换回家长模式',
+            icon: 'success'
+          })
+          // 跳转到家长首页
+          uni.reLaunch({
+            url: '/pages/index/parent-index'
+          })
+        }
+      } catch (e) {
+        uni.showToast({
+          title: '切换失败',
+          icon: 'none'
+        })
+      }
+    },
+    async approveTask(taskId) {
+      try {
+        await approveTask(taskId)
+        uni.showToast({
+          title: '已通过',
+          icon: 'success'
+        })
+        this.loadData()
+      } catch (e) {
+        uni.showToast({
+          title: '操作失败',
+          icon: 'none'
+        })
+      }
+    },
+    async rejectTask(taskId) {
+      try {
+        await rejectTask(taskId)
+        uni.showToast({
+          title: '已拒绝',
+          icon: 'success'
+        })
+        this.loadData()
+      } catch (e) {
+        uni.showToast({
+          title: '操作失败',
+          icon: 'none'
+        })
+      }
+    },
+    addTask() {
+      uni.navigateTo({
+        url: '/pages/tasks/create-task'
+      })
+    },
+    // 添加孩子
+    addChild() {
+      uni.navigateTo({
+        url: '/pages/profile/create-child'
+      })
+    },
+    goToReview() {
+      uni.navigateTo({
+        url: '/pages/tasks/review'
+      })
+    },
+    manageChildren() {
+      uni.navigateTo({
+        url: '/pages/profile/children'
+      })
+    },
+    switchMode() {
+      uni.showModal({
+        title: '切换模式',
+        content: '确定要切换模式吗?',
+        success: async (res) => {
+          if (res.confirm) {
+            const currentRole = this.$store.state.role
+            const newRole = currentRole === 'parent' ? 'child' : 'parent'
+            this.$store.commit('switchRole', newRole)
+            this.navigateToHome(newRole)
+            uni.showToast({
+              title: `已切换为${newRole === 'parent' ? '家长' : '孩子'}模式`,
+              icon: 'success'
+            })
+          }
+        }
+      })
+    },
+    navigateToHome(role) {
+      if (role === 'parent') {
+        uni.reLaunch({
+          url: '/pages/index/parent-index'
+        })
+      } else {
+        uni.reLaunch({
+          url: '/pages/index/child-index'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+}
+
+.welcome-section {
+  margin-bottom: 30rpx;
+}
+
+.hello {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.subtitle {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.section {
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
+}
+
+/* 孩子列表 */
+.children-list {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 20rpx;
+}
+
+.child-card {
+  display: flex;
+  align-items: center;
+  padding: 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.child-card:last-child {
+  border-bottom: none;
+}
+
+.child-avatar {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-right: 20rpx;
+}
+
+.child-info {
+  flex: 1;
+}
+
+.child-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+
+.child-points {
+  font-size: 24rpx;
+  color: #666;
+}
+
+.child-streak {
+  display: flex;
+  align-items: center;
+  background: #FFF5F5;
+  padding: 8rpx 16rpx;
+  border-radius: 20rpx;
+}
+
+.child-streak .fire {
+  font-size: 24rpx;
+}
+
+.child-streak .days {
+  color: #FF6B6B;
+  font-size: 22rpx;
+  margin-left: 4rpx;
+}
+
+/* 待审核任务 */
+.review-list {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 20rpx;
+}
+
+.review-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.review-item:last-child {
+  border-bottom: none;
+}
+
+.review-info {
+  flex: 1;
+}
+
+.task-name {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.task-child {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.review-actions {
+  display: flex;
+  gap: 10rpx;
+}
+
+.btn-pass, .btn-reject {
+  font-size: 24rpx;
+  padding: 10rpx 20rpx;
+  border-radius: 10rpx;
+  margin: 0;
+}
+
+.btn-pass {
+  background: #52C41A;
+  color: #fff;
+}
+
+.btn-reject {
+  background: #FF4D4F;
+  color: #fff;
+}
+
+.view-all-btn {
+  margin-top: 20rpx;
+  width: 100%;
+}
+
+/* 快捷操作 */
+.quick-actions {
+  display: flex;
+  justify-content: space-around;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  flex-wrap: wrap;
+}
+
+.action-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 25%;
+  margin-bottom: 20rpx;
+}
+
+.action-item .icon {
+  font-size: 48rpx;
+  margin-bottom: 10rpx;
+}
+
+.action-item .label {
+  font-size: 24rpx;
+  color: #666;
+}
+</style>

+ 160 - 31
zxyj-frontend/pages/login/login.vue

@@ -8,6 +8,44 @@
 
     <!-- 手机号登录 -->
     <view class="login-form">
+      <!-- 角色选择(新用户显示) -->
+      <view class="form-item" v-if="showRoleSelect">
+        <text class="label">我是</text>
+        <radio-group class="role-select" @change="onRoleChange">
+          <label class="role-option">
+            <radio value="parent" :checked="role === 'parent'" />
+            <text>家长</text>
+          </label>
+          <label class="role-option">
+            <radio value="child" :checked="role === 'child'" />
+            <text>孩子</text>
+          </label>
+        </radio-group>
+      </view>
+
+      <!-- 生日选择(新用户显示) -->
+      <view class="form-item" v-if="showRoleSelect">
+        <text class="label">生日</text>
+        <picker mode="date" :value="birthday" @change="onBirthdayChange">
+          <view class="picker-input">{{ birthday || '请选择生日' }}</view>
+        </picker>
+      </view>
+
+      <!-- 性别选择(新用户显示) -->
+      <view class="form-item" v-if="showRoleSelect">
+        <text class="label">性别</text>
+        <radio-group class="role-select" @change="onGenderChange">
+          <label class="role-option">
+            <radio value="male" :checked="gender === 'male'" />
+            <text>男</text>
+          </label>
+          <label class="role-option">
+            <radio value="female" :checked="gender === 'female'" />
+            <text>女</text>
+          </label>
+        </radio-group>
+      </view>
+
       <view class="form-item">
         <text class="label">手机号</text>
         <input 
@@ -80,7 +118,12 @@ export default {
       countdown: 0,
       agreed: false,
       nickname: '',
-      avatar: ''
+      avatar: '',
+      // 新增字段
+      role: 'parent',  // 角色选择
+      birthday: '',    // 生日
+      gender: '',      // 性别
+      showRoleSelect: true  // 显示角色选择
     }
   },
   onLoad() {
@@ -97,6 +140,23 @@ export default {
     handleImageError(e) {
       console.log('Logo加载失败,使用默认显示')
     },
+    onRoleChange(e) {
+      this.role = e.detail.value
+    },
+    onBirthdayChange(e) {
+      this.birthday = e.detail.value
+    },
+    onGenderChange(e) {
+      this.gender = e.detail.value
+    },
+    // 从微信获取昵称
+    getNicknameFromWechat() {
+      return this.nickname || '用户'
+    },
+    // 从微信获取头像
+    getAvatarFromWechat() {
+      return this.avatar || ''
+    },
     onAgreementChange(e) {
       this.agreed = e.detail.value.length > 0
     },
@@ -154,32 +214,50 @@ export default {
         return
       }
 
-      this.loading = true
-      try {
-        const res = await phoneLoginApi({
-          phone: this.phone,
-          code: this.code,
-          nickname: this.nickname,
-          avatar: this.avatar
-        })
-        
-        // 保存token
-        uni.setStorageSync('token', res.data.token)
-        uni.setStorageSync('userId', res.data.userId)
-        uni.setStorageSync('role', res.data.role)
-        uni.setStorageSync('familyId', res.data.familyId)
-        
-        uni.showToast({ title: '登录成功', icon: 'success' })
-        
-        // 跳转到首页
-        setTimeout(() => {
-          uni.switchTab({ url: '/pages/index/index' })
-        }, 1000)
-      } catch (e) {
-        uni.showToast({ title: e.message || '登录失败', icon: 'none' })
-      } finally {
-        this.loading = false
-      }
+		this.loading = true
+		try {
+			const res = await phoneLoginApi({
+				phone: this.phone,
+				code: this.code,
+				nickname: this.nickname || this.getNicknameFromWechat(),
+				avatar: this.avatar || this.getAvatarFromWechat(),
+				role: this.role,
+				birthday: this.birthday,
+				gender: this.gender
+			})
+
+			// 保存token和用户信息
+			uni.setStorageSync('token', res.data.token)
+			uni.setStorageSync('userId', res.data.userId)
+			uni.setStorageSync('role', res.data.role)
+			uni.setStorageSync('familyId', res.data.familyId)
+			uni.setStorageSync('userInfo', {
+				nickname: res.data.nickname,
+				userId: res.data.userId
+			})
+
+			uni.showToast({ title: '登录成功', icon: 'success' })
+
+			// 保存用户角色
+			const role = res.data.role || 'parent'
+
+			// 根据 isNewUser 决定跳转页面
+			setTimeout(() => {
+				if (res.data.isNewUser) {
+					// 新用户:跳转到信息编辑页面
+					uni.redirectTo({
+						url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}`
+					})
+				} else {
+					// 老用户:根据角色进入对应首页
+					this.navigateToHome(role)
+				}
+			}, 1000)
+		} catch (e) {
+			uni.showToast({ title: e.message || '登录失败', icon: 'none' })
+		} finally {
+			this.loading = false
+		}
     },
     async handleWechatPhoneLogin(e) {
       if (e.detail.errMsg === 'getPhoneNumber:ok') {
@@ -207,16 +285,32 @@ export default {
             avatar: this.avatar
           })
 
-          // 保存token
+          // 保存token和用户信息
           uni.setStorageSync('token', res.data.token)
           uni.setStorageSync('userId', res.data.userId)
           uni.setStorageSync('role', res.data.role)
           uni.setStorageSync('familyId', res.data.familyId)
-          
+          uni.setStorageSync('userInfo', {
+            nickname: res.data.nickname,
+            userId: res.data.userId
+          })
+
           uni.showToast({ title: '登录成功', icon: 'success' })
-          
+
+          // 保存用户角色
+          const role = res.data.role || 'parent'
+
+          // 根据 isNewUser 决定跳转页面
           setTimeout(() => {
-            uni.switchTab({ url: '/pages/index/index' })
+            if (res.data.isNewUser) {
+              // 新用户:跳转到信息编辑页面
+              uni.redirectTo({
+                url: `/pages/user-edit/user-edit?token=${res.data.token}&userId=${res.data.userId}`
+              })
+            } else {
+              // 老用户:根据角色进入对应首页
+              this.navigateToHome(role)
+            }
           }, 1000)
         } catch (e) {
           uni.showToast({ title: e.message || '登录失败', icon: 'none' })
@@ -224,6 +318,17 @@ export default {
           this.loading = false
         }
       }
+    },
+    navigateToHome(role) {
+      if (role === 'parent') {
+        uni.reLaunch({
+          url: '/pages/index/parent-index'
+        })
+      } else {
+        uni.reLaunch({
+          url: '/pages/index/child-index'
+        })
+      }
     }
   }
 }
@@ -325,6 +430,30 @@ export default {
   margin-top: 20rpx;
 }
 
+/* 角色选择器样式 */
+.role-select {
+  display: flex;
+  gap: 40rpx;
+  padding: 20rpx 0;
+}
+
+.role-option {
+  display: flex;
+  align-items: center;
+  gap: 10rpx;
+}
+
+.picker-input {
+  width: 100%;
+  height: 80rpx;
+  background: #f5f5f5;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  line-height: 80rpx;
+  color: #333;
+}
+
 .wechat-section {
   margin-top: 40rpx;
   text-align: center;

+ 19 - 0
zxyj-frontend/project.private.config.json

@@ -0,0 +1,19 @@
+{
+	"name": "心知家庭",
+	"version": "1.0.0",
+	"id": "",
+	"description": "家庭教育任务管理小程序",
+	"creator": "HBuilderX",
+	"platforms": ["mp-weixin"],
+	"h5": {},
+	"mp-weixin": {
+		"appid": "",
+		"setting": {
+			"urlCheck": false,
+			"es6": true,
+			"postcss": true,
+			"minified": true
+		},
+		"usingComponents": true
+	}
+}

+ 34 - 2
zxyj-frontend/utils/api.js

@@ -65,11 +65,31 @@ export const verifyPassword = (password) => {
 
 // 用户模块
 export const getUserInfo = () => {
-  return request('/api/user/info', 'GET')
+	return request('/api/user/info', 'GET')
+}
+
+export const updateUserInfo = (data) => {
+	return request('/api/user/update', 'POST', data)
+}
+
+export const joinFamily = (inviteCode) => {
+	return request('/api/user/join-family', 'POST', { inviteCode })
+}
+
+export const createFamily = (name) => {
+	return request('/api/user/create-family', 'POST', { name })
 }
 
 export const switchMode = (mode) => {
-  return request('/api/user/switch-mode', 'POST', { mode })
+	return request('/api/user/switch-mode', 'POST', { mode })
+}
+
+export const switchToChild = (childId) => {
+	return request('/api/user/switch-to-child', 'POST', { childId })
+}
+
+export const switchBackToParent = () => {
+	return request('/api/user/switch-back-to-parent', 'POST', {})
 }
 
 export const getChildren = () => {
@@ -93,6 +113,18 @@ export const getTodayTasks = (childId) => {
   return request('/api/tasks/today', 'GET', { childId })
 }
 
+export const getPendingReviewTasks = () => {
+  return request('/api/tasks/pending-review', 'GET')
+}
+
+export const approveTask = (taskId) => {
+  return request(`/api/tasks/${taskId}/review`, 'POST', { approved: true })
+}
+
+export const rejectTask = (taskId) => {
+  return request(`/api/tasks/${taskId}/review`, 'POST', { approved: false })
+}
+
 export const completeTask = (taskId, childId, photoUrl) => {
   return request(`/api/tasks/${taskId}/complete`, 'POST', { childId, photoUrl })
 }