|
|
@@ -0,0 +1,1718 @@
|
|
|
+# �亥��𠰴振 MVP 摰䂿緵霈∪�
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 摰䂿緵敹�䰻�𠰴振撠讐�摨𦬅VP��𧋦嚗���砌遙�∠恣����妖��頂蝏麄����曹�蝟颯���璅∪���揢蝑㗇瓲敹���賬�?
|
|
|
+**Architecture:** 敺桐縑撠讐�摨誩�蝡?+ Java Spring Boot�𡒊垢 + MySQL�唳旿摨瓐����沖ESTful API餈𥡝��滚�蝡舫�帋縑嚗𥇍WT餈𥡝�頨思遢霈方��?
|
|
|
+**Tech Stack:**
|
|
|
+- �滨垢嚗𡁜凝靽∪�蝔见��毺�撘��?- �𡒊垢嚗麿ava 1.8 + Spring Boot 2.7 + MyBatis
|
|
|
+- �唳旿摨橒�MySQL 8.0
|
|
|
+- 霈方�嚗麿WT
|
|
|
+- ��﹝嚗锭wagger 3.0
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ��辣蝏𤘪�
|
|
|
+
|
|
|
+```
|
|
|
+E:\workspace\dan\zxyj\
|
|
|
+�鎿��� backend/ # Java�𡒊垢
|
|
|
+�? �鎿��� pom.xml
|
|
|
+�? �鎿��� src/main/java/com/xinzhiyijia/
|
|
|
+�? �? �鎿��� XinzhiyijiaApplication.java
|
|
|
+�? �? �鎿��� config/ # �滨蔭蝐?�? �? �? �鎿��� WebMvcConfig.java
|
|
|
+�? �? �? �鎿��� JwtConfig.java
|
|
|
+�? �? �? �婙��� SwaggerConfig.java
|
|
|
+�? �? �鎿��� common/ # �砍�蝐?�? �? �? �鎿��� Result.java
|
|
|
+�? �? �? �鎿��� ResultCode.java
|
|
|
+�? �? �? �婙��� JwtUtil.java
|
|
|
+�? �? �鎿��� entity/ # 摰硺�蝐?�? �? �? �鎿��� User.java
|
|
|
+�? �? �? �鎿��� Family.java
|
|
|
+�? �? �? �鎿��� Child.java
|
|
|
+�? �? �? �鎿��� Task.java
|
|
|
+�? �? �? �鎿��� PointsLog.java
|
|
|
+�? �? �? �鎿��� Reward.java
|
|
|
+�? �? �? �婙��� RewardCategory.java
|
|
|
+�? �? �鎿��� dto/ # �唳旿隡㰘�撖寡情
|
|
|
+�? �? �鎿��� mapper/ # MyBatis Mapper
|
|
|
+�? �? �? �鎿��� UserMapper.java
|
|
|
+�? �? �? �鎿��� FamilyMapper.java
|
|
|
+�? �? �? �鎿��� ChildMapper.java
|
|
|
+�? �? �? �鎿��� TaskMapper.java
|
|
|
+�? �? �? �鎿��� PointsLogMapper.java
|
|
|
+�? �? �? �婙��� RewardMapper.java
|
|
|
+�? �? �鎿��� service/ # 銝𡁜𦛚�餉�
|
|
|
+�? �? �? �鎿��� UserService.java
|
|
|
+�? �? �? �鎿��� FamilyService.java
|
|
|
+�? �? �? �鎿��� TaskService.java
|
|
|
+�? �? �? �鎿��� PointsService.java
|
|
|
+�? �? �? �婙��� RewardService.java
|
|
|
+�? �? �婙��� controller/ # �批��?�? �? �鎿��� AuthController.java
|
|
|
+�? �? �鎿��� UserController.java
|
|
|
+�? �? �鎿��� FamilyController.java
|
|
|
+�? �? �鎿��� TaskController.java
|
|
|
+�? �? �鎿��� PointsController.java
|
|
|
+�? �? �婙��� RewardController.java
|
|
|
+�? �婙��� src/main/resources/
|
|
|
+�? �鎿��� application.yml
|
|
|
+�? �婙��� mapper/ # MyBatis XML
|
|
|
+�? �鎿��� UserMapper.xml
|
|
|
+�? �鎿��� FamilyMapper.xml
|
|
|
+�? �鎿��� ChildMapper.xml
|
|
|
+�? �鎿��� TaskMapper.xml
|
|
|
+�? �鎿��� PointsLogMapper.xml
|
|
|
+�? �婙��� RewardMapper.xml
|
|
|
+�?�婙��� miniprogram/ # 敺桐縑撠讐�摨誩�蝡? �鎿��� app.js
|
|
|
+ �鎿��� app.json
|
|
|
+ �鎿��� app.wxss
|
|
|
+ �鎿��� pages/
|
|
|
+ �? �鎿��� index/ # 擐㚚△嚗���脣�瘚��
|
|
|
+ �? �鎿��� login/ # �餃�憿? �? �鎿��� parent-home/ # 摰園鵭蝡舫�憿? �? �鎿��� child-home/ # 摮拙�蝡舫�憿? �? �鎿��� task-list/ # 隞餃𦛚�𡑒”
|
|
|
+ �? �鎿��� task-detail/ # 隞餃𦛚霂行�
|
|
|
+ �? �鎿��� task-create/ # �穃�隞餃𦛚
|
|
|
+ �? �鎿��� reward-list/ # 憟硋��𡑒”
|
|
|
+ �? �鎿��� reward-create/ # 瘛餃�憟硋�
|
|
|
+ �? �鎿��� points-log/ # 蝘臬�瘚�偌
|
|
|
+ �? �婙��� settings/ # 霈曄蔭
|
|
|
+ �鎿��� components/ # �砍�蝏�辣
|
|
|
+ �? �鎿��� task-card/
|
|
|
+ �? �鎿��� reward-card/
|
|
|
+ �? �婙��� countdown/
|
|
|
+ �鎿��� utils/
|
|
|
+ �? �鎿��� api.js # API霂瑟�撠��
|
|
|
+ �? �鎿��� auth.js # 霈方�撌亙�
|
|
|
+ �? �婙��� util.js
|
|
|
+ �婙��� images/ # �曄�韏��
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 1: �唳旿摨栞挽霈∩�撱箄”
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/main/resources/sql/init.sql`
|
|
|
+- Create: `backend/src/main/resources/sql/seed.sql`
|
|
|
+
|
|
|
+### Step 1: 蝻硋��唳旿摨枏�憪见�SQL
|
|
|
+
|
|
|
+�𥕦遣 `backend/src/main/resources/sql/init.sql`嚗?
|
|
|
+```sql
|
|
|
+-- 敹�䰻�𠰴振�唳旿摨枏�憪见��𡁏𧋦
|
|
|
+
|
|
|
+-- 1. �冽�銵?CREATE TABLE IF NOT EXISTS `users` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `openid` VARCHAR(64) NOT NULL COMMENT '敺桐縑openid',
|
|
|
+ `phone` VARCHAR(20) COMMENT '�𧢲㦤�?,
|
|
|
+ `nickname` VARCHAR(50) COMMENT '�萇妍',
|
|
|
+ `avatar` VARCHAR(255) COMMENT '憭游�URL',
|
|
|
+ `role` ENUM('parent', 'child') NOT NULL COMMENT '閫坿𠧧嚗𡁜振�?摮拙�',
|
|
|
+ `password` VARCHAR(100) COMMENT '撖��嚗��撖��',
|
|
|
+ `family_id` BIGINT COMMENT '��撅𧼮振摨背D',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY `uk_openid` (`openid`),
|
|
|
+ INDEX `idx_family_id` (`family_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='�冽�銵?;
|
|
|
+
|
|
|
+-- 2. 摰嗅滬銵?CREATE TABLE IF NOT EXISTS `families` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `name` VARCHAR(50) NOT NULL COMMENT '摰嗅滬�滨妍',
|
|
|
+ `invite_code` VARCHAR(10) NOT NULL COMMENT '��霂瑞�',
|
|
|
+ `creator_id` BIGINT NOT NULL COMMENT '�𥕦遣��D',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ UNIQUE KEY `uk_invite_code` (`invite_code`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='摰嗅滬銵?;
|
|
|
+
|
|
|
+-- 3. 摮拙�靽⊥�銵?CREATE TABLE IF NOT EXISTS `children` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `user_id` BIGINT NOT NULL COMMENT '�唾��冽�ID',
|
|
|
+ `family_id` BIGINT NOT NULL COMMENT '��撅𧼮振摨背D',
|
|
|
+ `age` INT NOT NULL COMMENT '撟湧�',
|
|
|
+ `grade` VARCHAR(20) COMMENT '撟渡漣',
|
|
|
+ `dan_level` VARCHAR(10) COMMENT 'DAN瘚贝�蝑厩漣',
|
|
|
+ `penalty_enabled` TINYINT(1) DEFAULT 1 COMMENT '�臬炏撘��舀緍���6撗�誑銝钅�霈文��哨�',
|
|
|
+ `current_streak` INT DEFAULT 0 COMMENT '敶枏�餈䂿賒憭拇㺭',
|
|
|
+ `max_streak` INT DEFAULT 0 COMMENT '��蟮���輯�蝏剖予�?,
|
|
|
+ `last_check_in_date` DATE COMMENT '���擧��⊥𠯫�?,
|
|
|
+ `total_points` INT DEFAULT 0 COMMENT '敶枏�蝘臬�',
|
|
|
+ `total_earned` INT DEFAULT 0 COMMENT '蝝航恣�瑕�蝘臬�',
|
|
|
+ `theme` VARCHAR(20) DEFAULT 'default' COMMENT '銝駁�',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ INDEX `idx_family_id` (`family_id`),
|
|
|
+ INDEX `idx_user_id` (`user_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='摮拙�靽⊥�銵?;
|
|
|
+
|
|
|
+-- 4. 隞餃𦛚銵?CREATE TABLE IF NOT EXISTS `tasks` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `family_id` BIGINT NOT NULL COMMENT '��撅𧼮振摨?,
|
|
|
+ `creator_id` BIGINT NOT NULL COMMENT '�𥕦遣���摰園鵭嚗?,
|
|
|
+ `child_id` BIGINT NOT NULL COMMENT '�扯�摮拙�',
|
|
|
+ `title` VARCHAR(100) NOT NULL COMMENT '隞餃𦛚�滨妍',
|
|
|
+ `description` TEXT COMMENT '憭�釣霂湔�',
|
|
|
+ `points` INT NOT NULL DEFAULT 2 COMMENT '蝘臬��堆�1-10嚗?,
|
|
|
+ `category` VARCHAR(20) NOT NULL COMMENT '��掩嚗𡁜郎銋?��暑/餈𣂼𢆡/�嗡�',
|
|
|
+ `priority` VARCHAR(10) DEFAULT 'medium' COMMENT '隡睃�蝥改�high/medium/low',
|
|
|
+ `deadline` DATETIME NOT NULL COMMENT '�芣迫�園𡢿',
|
|
|
+ `estimated_duration` INT COMMENT '憸�恣�園鵭嚗�����',
|
|
|
+ `repeat_type` VARCHAR(20) DEFAULT 'none' COMMENT '�滚�蝐餃�嚗䭰one/daily/weekly',
|
|
|
+ `need_review` TINYINT(1) DEFAULT 0 COMMENT '�臬炏��閬�恣�?,
|
|
|
+ `status` VARCHAR(20) DEFAULT 'pending' COMMENT '�嗆���pending/completed/approved/rejected/overdue',
|
|
|
+ `completed_at` DATETIME COMMENT '摰峕��園𡢿',
|
|
|
+ `completed_early` TINYINT(1) DEFAULT 0 COMMENT '�臬炏�𣂼�摰峕�',
|
|
|
+ `review_photo_url` VARCHAR(255) COMMENT '摰⊥瓲�抒��啣�',
|
|
|
+ `review_voice_url` VARCHAR(255) COMMENT '摰⊥瓲霂剝𨺗�啣�',
|
|
|
+ `review_status` VARCHAR(20) COMMENT '摰⊥瓲�嗆���pending/reviewing/approved/rejected',
|
|
|
+ `review_comment` VARCHAR(500) COMMENT '摰⊥瓲憭�釣',
|
|
|
+ `reviewed_at` DATETIME COMMENT '摰⊥瓲�園𡢿',
|
|
|
+ `reviewed_by` BIGINT COMMENT '摰⊥瓲鈭截D',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ INDEX `idx_family_id` (`family_id`),
|
|
|
+ INDEX `idx_child_id` (`child_id`),
|
|
|
+ INDEX `idx_status` (`status`),
|
|
|
+ INDEX `idx_deadline` (`deadline`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隞餃𦛚銵?;
|
|
|
+
|
|
|
+-- 5. 蝘臬�瘚�偌銵?CREATE TABLE IF NOT EXISTS `points_log` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `child_id` BIGINT NOT NULL COMMENT '摮拙�ID',
|
|
|
+ `task_id` BIGINT COMMENT '�唾�隞餃𦛚ID',
|
|
|
+ `amount` INT NOT NULL COMMENT '蝘臬��堆�甇�㺭銝箄繮敺梹�韐�㺭銝箸緍�歹�',
|
|
|
+ `type` VARCHAR(20) NOT NULL COMMENT '蝐餃�嚗魩ask/early/streak/focus/reward/correction',
|
|
|
+ `description` VARCHAR(200) NOT NULL COMMENT '�讛膩',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX `idx_child_id` (`child_id`),
|
|
|
+ INDEX `idx_created_at` (`created_at`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='蝘臬�瘚�偌銵?;
|
|
|
+
|
|
|
+-- 6. 憟硋���掩銵?CREATE TABLE IF NOT EXISTS `reward_categories` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `family_id` BIGINT NOT NULL COMMENT '��撅𧼮振摨?,
|
|
|
+ `name` VARCHAR(50) NOT NULL COMMENT '��掩�滨妍',
|
|
|
+ `icon` VARCHAR(50) COMMENT '�暹�',
|
|
|
+ `sort_order` INT DEFAULT 0 COMMENT '�鍦�',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX `idx_family_id` (`family_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='憟硋���掩銵?;
|
|
|
+
|
|
|
+-- 7. 憟硋�銵?CREATE TABLE IF NOT EXISTS `rewards` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `family_id` BIGINT NOT NULL COMMENT '��撅𧼮振摨?,
|
|
|
+ `child_id` BIGINT COMMENT '���摮拙�嚗𠃊ULL銵函內�典��舐鍂嚗?,
|
|
|
+ `category_id` BIGINT COMMENT '��掩ID',
|
|
|
+ `name` VARCHAR(100) NOT NULL COMMENT '憟硋��滨妍',
|
|
|
+ `description` VARCHAR(500) COMMENT '憟硋��讛膩',
|
|
|
+ `points_required` INT NOT NULL COMMENT '����蝘臬�',
|
|
|
+ `icon` VARCHAR(50) COMMENT '�暹�',
|
|
|
+ `stock` INT DEFAULT -1 COMMENT '摨枏�嚗?1銵函內�𣳇�嚗?,
|
|
|
+ `status` VARCHAR(20) DEFAULT 'active' COMMENT '�嗆���active/inactive',
|
|
|
+ `is_default` TINYINT(1) DEFAULT 0 COMMENT '�臬炏蝟餌�憸�挽',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ INDEX `idx_family_id` (`family_id`),
|
|
|
+ INDEX `idx_child_id` (`child_id`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='憟硋�銵?;
|
|
|
+
|
|
|
+-- 8. 憟硋��烐揢銵?CREATE TABLE IF NOT EXISTS `reward_exchanges` (
|
|
|
+ `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `reward_id` BIGINT NOT NULL COMMENT '憟硋�ID',
|
|
|
+ `child_id` BIGINT NOT NULL COMMENT '摮拙�ID',
|
|
|
+ `points_used` INT NOT NULL COMMENT '瘨��㛖妖�?,
|
|
|
+ `status` VARCHAR(20) DEFAULT 'pending' COMMENT '�嗆���pending/approved/rejected',
|
|
|
+ `review_comment` VARCHAR(500) COMMENT '摰⊥瓲憭�釣',
|
|
|
+ `reviewed_by` BIGINT COMMENT '摰⊥瓲鈭截D',
|
|
|
+ `reviewed_at` DATETIME COMMENT '摰⊥瓲�園𡢿',
|
|
|
+ `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX `idx_child_id` (`child_id`),
|
|
|
+ INDEX `idx_status` (`status`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='憟硋��烐揢銵?;
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: 蝻硋�蝘滚��唳旿SQL
|
|
|
+
|
|
|
+�𥕦遣 `backend/src/main/resources/sql/seed.sql`嚗?
|
|
|
+```sql
|
|
|
+-- 敹�䰻�𠰴振蝘滚��唳旿
|
|
|
+
|
|
|
+-- 暺䁅恕憟硋���掩
|
|
|
+INSERT INTO `reward_categories` (`family_id`, `name`, `icon`, `sort_order`) VALUES
|
|
|
+(-1, '�抵捶蝐?, '��', 1),
|
|
|
+(-1, '蝎曄�蝐?, '�𤁗', 2),
|
|
|
+(-1, '�寞�蝐?, '��', 3);
|
|
|
+
|
|
|
+-- 暺䁅恕憸�挽憟硋�
|
|
|
+INSERT INTO `rewards` (`family_id`, `child_id`, `category_id`, `name`, `points_required`, `icon`, `is_default`) VALUES
|
|
|
+(-1, NULL, 1, '撠誯妟憌?, 10, '�斆', 1),
|
|
|
+(-1, NULL, 1, '撠讐焵�?, 30, '�孛', 1),
|
|
|
+(-1, NULL, 1, '�拙��脩�', 50, '��', 1),
|
|
|
+(-1, NULL, 2, '�衤���𢆡�餌�', 5, '�𤁗', 1),
|
|
|
+(-1, NULL, 2, '�拇虜�?0���', 15, '�緤', 1),
|
|
|
+(-1, NULL, 2, '�餅虜銋𣂼㦤', 200, '�䔝', 1),
|
|
|
+(-1, NULL, 3, '隞𠰴予銝齿�蝣?, 15, '�椉儭?, 1),
|
|
|
+(-1, NULL, 3, '�𡁶辺30���', 20, '��', 1),
|
|
|
+(-1, NULL, 3, '�滚�銝�憿孵振�?, 25, '�?, 1);
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 2: �𡒊垢憿寧𤌍�嘥��?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/pom.xml`
|
|
|
+- Create: `backend/src/main/resources/application.yml`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/XinzhiyijiaApplication.java`
|
|
|
+
|
|
|
+### Step 1: �𥕦遣pom.xml
|
|
|
+
|
|
|
+```xml
|
|
|
+<?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>
|
|
|
+
|
|
|
+ <groupId>com.xinzhiyijia</groupId>
|
|
|
+ <artifactId>xinzhiyijia-backend</artifactId>
|
|
|
+ <version>1.0.0</version>
|
|
|
+ <packaging>jar</packaging>
|
|
|
+
|
|
|
+ <parent>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-parent</artifactId>
|
|
|
+ <version>2.7.18</version>
|
|
|
+ </parent>
|
|
|
+
|
|
|
+ <properties>
|
|
|
+ <java.version>1.8</java.version>
|
|
|
+ <mybatis.version>2.3.2</mybatis.version>
|
|
|
+ </properties>
|
|
|
+
|
|
|
+ <dependencies>
|
|
|
+ <!-- Spring Boot Web -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-web</artifactId>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- MyBatis -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.mybatis.spring.boot</groupId>
|
|
|
+ <artifactId>mybatis-spring-boot-starter</artifactId>
|
|
|
+ <version>${mybatis.version}</version>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- MySQL -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>mysql</groupId>
|
|
|
+ <artifactId>mysql-connector-java</artifactId>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Swagger -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.springdoc</groupId>
|
|
|
+ <artifactId>springdoc-openapi-ui</artifactId>
|
|
|
+ <version>1.7.0</version>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- JWT -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>io.jsonwebtoken</groupId>
|
|
|
+ <artifactId>jjwt</artifactId>
|
|
|
+ <version>0.9.1</version>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Lombok -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.projectlombok</groupId>
|
|
|
+ <artifactId>lombok</artifactId>
|
|
|
+ <optional>true</optional>
|
|
|
+ </dependency>
|
|
|
+
|
|
|
+ <!-- Test -->
|
|
|
+ <dependency>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-starter-test</artifactId>
|
|
|
+ <scope>test</scope>
|
|
|
+ </dependency>
|
|
|
+ </dependencies>
|
|
|
+
|
|
|
+ <build>
|
|
|
+ <plugins>
|
|
|
+ <plugin>
|
|
|
+ <groupId>org.springframework.boot</groupId>
|
|
|
+ <artifactId>spring-boot-maven-plugin</artifactId>
|
|
|
+ </plugin>
|
|
|
+ </plugins>
|
|
|
+ </build>
|
|
|
+</project>
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: �𥕦遣application.yml
|
|
|
+
|
|
|
+```yaml
|
|
|
+server:
|
|
|
+ port: 8080
|
|
|
+ servlet:
|
|
|
+ context-path: /api
|
|
|
+
|
|
|
+spring:
|
|
|
+ application:
|
|
|
+ name: xinzhiyijia-backend
|
|
|
+ datasource:
|
|
|
+ driver-class-name: com.mysql.cj.jdbc.Driver
|
|
|
+ url: jdbc:mysql://localhost:3306/xinzhiyijia?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
|
|
|
+ username: root
|
|
|
+ password: root
|
|
|
+
|
|
|
+mybatis:
|
|
|
+ mapper-locations: classpath:mapper/*.xml
|
|
|
+ type-aliases-package: com.xinzhiyijia.entity
|
|
|
+ configuration:
|
|
|
+ map-underscore-to-camel-case: true
|
|
|
+
|
|
|
+jwt:
|
|
|
+ secret: xinzhiyijia-secret-key-2024
|
|
|
+ expiration: 604800000
|
|
|
+
|
|
|
+xinzhiyijia:
|
|
|
+ penalty:
|
|
|
+ max-daily: 5
|
|
|
+ grace-minutes: 10
|
|
|
+ points:
|
|
|
+ early-bonus: 1
|
|
|
+ focus-perfect-bonus: 1
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: �𥕦遣�臬𢆡蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia;
|
|
|
+
|
|
|
+import org.mybatis.spring.annotation.MapperScan;
|
|
|
+import org.springframework.boot.SpringApplication;
|
|
|
+import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
|
+
|
|
|
+@SpringBootApplication
|
|
|
+@MapperScan("com.xinzhiyijia.mapper")
|
|
|
+public class XinzhiyijiaApplication {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ SpringApplication.run(XinzhiyijiaApplication.class, args);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 3: �砍�蝐颱�撌亙�蝐?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/common/Result.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/common/ResultCode.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/common/JwtUtil.java`
|
|
|
+
|
|
|
+### Step 1: �𥕦遣蝏煺��滚�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.common;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class Result<T> {
|
|
|
+ private int code;
|
|
|
+ private String message;
|
|
|
+ private T data;
|
|
|
+
|
|
|
+ public static <T> Result<T> success(T data) {
|
|
|
+ Result<T> result = new Result<>();
|
|
|
+ result.setCode(200);
|
|
|
+ result.setMessage("success");
|
|
|
+ result.setData(data);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> Result<T> success(String message, T data) {
|
|
|
+ Result<T> result = new Result<>();
|
|
|
+ result.setCode(200);
|
|
|
+ result.setMessage(message);
|
|
|
+ result.setData(data);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> Result<T> error(int code, String message) {
|
|
|
+ Result<T> result = new Result<>();
|
|
|
+ result.setCode(code);
|
|
|
+ result.setMessage(message);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> Result<T> error(String message) {
|
|
|
+ return error(500, message);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: �𥕦遣�嗆����帋蜀
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.common;
|
|
|
+
|
|
|
+public enum ResultCode {
|
|
|
+ SUCCESS(200, "�𣂼�"),
|
|
|
+ BAD_REQUEST(400, "霂瑟���㺭�躰秤"),
|
|
|
+ UNAUTHORIZED(401, "�芣��?),
|
|
|
+ FORBIDDEN(403, "蝳�迫霈輸䔮"),
|
|
|
+ NOT_FOUND(404, "韏��銝滚��?),
|
|
|
+ INTERNAL_ERROR(500, "�滚𦛚�典��券�霂?);
|
|
|
+
|
|
|
+ private final int code;
|
|
|
+ private final String message;
|
|
|
+
|
|
|
+ ResultCode(int code, String message) {
|
|
|
+ this.code = code;
|
|
|
+ this.message = message;
|
|
|
+ }
|
|
|
+
|
|
|
+ public int getCode() { return code; }
|
|
|
+ public String getMessage() { return message; }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: �𥕦遣JWT撌亙�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.common;
|
|
|
+
|
|
|
+import io.jsonwebtoken.*;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+@Component
|
|
|
+public class JwtUtil {
|
|
|
+
|
|
|
+ @Value("${jwt.secret}")
|
|
|
+ private String secret;
|
|
|
+
|
|
|
+ @Value("${jwt.expiration}")
|
|
|
+ private Long expiration;
|
|
|
+
|
|
|
+ public String generateToken(Long userId, String role) {
|
|
|
+ return Jwts.builder()
|
|
|
+ .setSubject(String.valueOf(userId))
|
|
|
+ .claim("role", role)
|
|
|
+ .setIssuedAt(new Date())
|
|
|
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
|
|
|
+ .signWith(SignatureAlgorithm.HS256, secret)
|
|
|
+ .compact();
|
|
|
+ }
|
|
|
+
|
|
|
+ public Claims parseToken(String token) {
|
|
|
+ try {
|
|
|
+ return Jwts.parser()
|
|
|
+ .setSigningKey(secret)
|
|
|
+ .parseClaimsJws(token)
|
|
|
+ .getBody();
|
|
|
+ } catch (ExpiredJwtException e) {
|
|
|
+ throw new RuntimeException("Token撌脰��?);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("Token�䭾�");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Long getUserIdFromToken(String token) {
|
|
|
+ Claims claims = parseToken(token);
|
|
|
+ return Long.parseLong(claims.getSubject());
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getRoleFromToken(String token) {
|
|
|
+ Claims claims = parseToken(token);
|
|
|
+ return claims.get("role", String.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isTokenValid(String token) {
|
|
|
+ try {
|
|
|
+ parseToken(token);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 4: 摰硺�蝐餃�撱?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/entity/User.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/entity/Family.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/entity/Child.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/entity/Task.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/entity/PointsLog.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/entity/Reward.java`
|
|
|
+
|
|
|
+### Step 1: �冽�摰硺�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.entity;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class User {
|
|
|
+ private Long id;
|
|
|
+ private String openid;
|
|
|
+ private String phone;
|
|
|
+ private String nickname;
|
|
|
+ private String avatar;
|
|
|
+ private String role; // parent, child
|
|
|
+ private String password;
|
|
|
+ private Long familyId;
|
|
|
+ private LocalDateTime createdAt;
|
|
|
+ private LocalDateTime updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: 摰嗅滬摰硺�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.entity;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class Family {
|
|
|
+ private Long id;
|
|
|
+ private String name;
|
|
|
+ private String inviteCode;
|
|
|
+ private Long creatorId;
|
|
|
+ private LocalDateTime createdAt;
|
|
|
+ private LocalDateTime updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: 摮拙�摰硺�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.entity;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class Child {
|
|
|
+ private Long id;
|
|
|
+ private Long userId;
|
|
|
+ private Long familyId;
|
|
|
+ private Integer age;
|
|
|
+ private String grade;
|
|
|
+ private String danLevel;
|
|
|
+ private Boolean penaltyEnabled;
|
|
|
+ private Integer currentStreak;
|
|
|
+ private Integer maxStreak;
|
|
|
+ private LocalDate lastCheckInDate;
|
|
|
+ private Integer totalPoints;
|
|
|
+ private Integer totalEarned;
|
|
|
+ private String theme;
|
|
|
+ private LocalDateTime createdAt;
|
|
|
+ private LocalDateTime updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 4: 隞餃𦛚摰硺�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.entity;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class Task {
|
|
|
+ private Long id;
|
|
|
+ private Long familyId;
|
|
|
+ private Long creatorId;
|
|
|
+ private Long childId;
|
|
|
+ private String title;
|
|
|
+ private String description;
|
|
|
+ private Integer points;
|
|
|
+ private String category;
|
|
|
+ private String priority;
|
|
|
+ private LocalDateTime deadline;
|
|
|
+ private Integer estimatedDuration;
|
|
|
+ private String repeatType;
|
|
|
+ private Boolean needReview;
|
|
|
+ private String status;
|
|
|
+ private LocalDateTime completedAt;
|
|
|
+ private Boolean completedEarly;
|
|
|
+ private String reviewPhotoUrl;
|
|
|
+ private String reviewVoiceUrl;
|
|
|
+ private String reviewStatus;
|
|
|
+ private String reviewComment;
|
|
|
+ private LocalDateTime reviewedAt;
|
|
|
+ private Long reviewedBy;
|
|
|
+ private LocalDateTime createdAt;
|
|
|
+ private LocalDateTime updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 5: 蝘臬�瘚�偌摰硺�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.entity;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class PointsLog {
|
|
|
+ private Long id;
|
|
|
+ private Long childId;
|
|
|
+ private Long taskId;
|
|
|
+ private Integer amount;
|
|
|
+ private String type; // task/early/streak/focus/reward/correction
|
|
|
+ private String description;
|
|
|
+ private LocalDateTime createdAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 6: 憟硋�摰硺�蝐?
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.entity;
|
|
|
+
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class Reward {
|
|
|
+ private Long id;
|
|
|
+ private Long familyId;
|
|
|
+ private Long childId;
|
|
|
+ private Long categoryId;
|
|
|
+ private String name;
|
|
|
+ private String description;
|
|
|
+ private Integer pointsRequired;
|
|
|
+ private String icon;
|
|
|
+ private Integer stock;
|
|
|
+ private String status;
|
|
|
+ private Boolean isDefault;
|
|
|
+ private LocalDateTime createdAt;
|
|
|
+ private LocalDateTime updatedAt;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 5: Mapper撅���?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/mapper/UserMapper.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/mapper/FamilyMapper.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/mapper/ChildMapper.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/mapper/TaskMapper.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/mapper/PointsLogMapper.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/mapper/RewardMapper.java`
|
|
|
+- Create: `backend/src/main/resources/mapper/UserMapper.xml`
|
|
|
+- Create: `backend/src/main/resources/mapper/TaskMapper.xml`
|
|
|
+- Create: `backend/src/main/resources/mapper/PointsLogMapper.xml`
|
|
|
+- Create: `backend/src/main/resources/mapper/RewardMapper.xml`
|
|
|
+
|
|
|
+### Step 1: UserMapper�亙藁
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.mapper;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.User;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+import org.apache.ibatis.annotations.Param;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface UserMapper {
|
|
|
+ User findById(@Param("id") Long id);
|
|
|
+ User findByOpenid(@Param("openid") String openid);
|
|
|
+ int insert(User user);
|
|
|
+ int update(User user);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: UserMapper.xml
|
|
|
+
|
|
|
+```xml
|
|
|
+<?xml version="1.0" encoding="UTF-8"?>
|
|
|
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
|
+<mapper namespace="com.xinzhiyijia.mapper.UserMapper">
|
|
|
+
|
|
|
+ <resultMap id="UserMap" type="com.xinzhiyijia.entity.User">
|
|
|
+ <id column="id" property="id"/>
|
|
|
+ <result column="openid" property="openid"/>
|
|
|
+ <result column="phone" property="phone"/>
|
|
|
+ <result column="nickname" property="nickname"/>
|
|
|
+ <result column="avatar" property="avatar"/>
|
|
|
+ <result column="role" property="role"/>
|
|
|
+ <result column="password" property="password"/>
|
|
|
+ <result column="family_id" property="familyId"/>
|
|
|
+ <result column="created_at" property="createdAt"/>
|
|
|
+ <result column="updated_at" property="updatedAt"/>
|
|
|
+ </resultMap>
|
|
|
+
|
|
|
+ <select id="findById" resultMap="UserMap">
|
|
|
+ SELECT * FROM users WHERE id = #{id}
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <select id="findByOpenid" resultMap="UserMap">
|
|
|
+ SELECT * FROM users WHERE openid = #{openid}
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
|
|
+ INSERT INTO users (openid, phone, nickname, avatar, role, password, family_id)
|
|
|
+ VALUES (#{openid}, #{phone}, #{nickname}, #{avatar}, #{role}, #{password}, #{familyId})
|
|
|
+ </insert>
|
|
|
+
|
|
|
+ <update id="update">
|
|
|
+ UPDATE users
|
|
|
+ SET phone = #{phone},
|
|
|
+ nickname = #{nickname},
|
|
|
+ avatar = #{avatar},
|
|
|
+ password = #{password},
|
|
|
+ family_id = #{familyId},
|
|
|
+ updated_at = NOW()
|
|
|
+ WHERE id = #{id}
|
|
|
+ </update>
|
|
|
+
|
|
|
+</mapper>
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: TaskMapper�亙藁
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.mapper;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.Task;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+import org.apache.ibatis.annotations.Param;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface TaskMapper {
|
|
|
+ Task findById(@Param("id") Long id);
|
|
|
+ List<Task> findByChildId(@Param("childId") Long childId, @Param("date") String date);
|
|
|
+ List<Task> findByFamilyId(@Param("familyId") Long familyId);
|
|
|
+ int insert(Task task);
|
|
|
+ int update(Task task);
|
|
|
+ int updateStatus(@Param("id") Long id, @Param("status") String status);
|
|
|
+ int countTodayCompleted(@Param("childId") Long childId, @Param("date") String date);
|
|
|
+ int countTodayTotal(@Param("childId") Long childId, @Param("date") String date);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 4: TaskMapper.xml
|
|
|
+
|
|
|
+```xml
|
|
|
+<?xml version="1.0" encoding="UTF-8"?>
|
|
|
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
|
+<mapper namespace="com.xinzhiyijia.mapper.TaskMapper">
|
|
|
+
|
|
|
+ <resultMap id="TaskMap" type="com.xinzhiyijia.entity.Task">
|
|
|
+ <id column="id" property="id"/>
|
|
|
+ <result column="family_id" property="familyId"/>
|
|
|
+ <result column="creator_id" property="creatorId"/>
|
|
|
+ <result column="child_id" property="childId"/>
|
|
|
+ <result column="title" property="title"/>
|
|
|
+ <result column="description" property="description"/>
|
|
|
+ <result column="points" property="points"/>
|
|
|
+ <result column="category" property="category"/>
|
|
|
+ <result column="priority" property="priority"/>
|
|
|
+ <result column="deadline" property="deadline"/>
|
|
|
+ <result column="estimated_duration" property="estimatedDuration"/>
|
|
|
+ <result column="repeat_type" property="repeatType"/>
|
|
|
+ <result column="need_review" property="needReview"/>
|
|
|
+ <result column="status" property="status"/>
|
|
|
+ <result column="completed_at" property="completedAt"/>
|
|
|
+ <result column="completed_early" property="completedEarly"/>
|
|
|
+ <result column="review_photo_url" property="reviewPhotoUrl"/>
|
|
|
+ <result column="review_voice_url" property="reviewVoiceUrl"/>
|
|
|
+ <result column="review_status" property="reviewStatus"/>
|
|
|
+ <result column="review_comment" property="reviewComment"/>
|
|
|
+ <result column="reviewed_at" property="reviewedAt"/>
|
|
|
+ <result column="reviewed_by" property="reviewedBy"/>
|
|
|
+ <result column="created_at" property="createdAt"/>
|
|
|
+ <result column="updated_at" property="updatedAt"/>
|
|
|
+ </resultMap>
|
|
|
+
|
|
|
+ <select id="findById" resultMap="TaskMap">
|
|
|
+ SELECT * FROM tasks WHERE id = #{id}
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <select id="findByChildId" resultMap="TaskMap">
|
|
|
+ SELECT * FROM tasks
|
|
|
+ WHERE child_id = #{childId}
|
|
|
+ AND DATE(deadline) = #{date}
|
|
|
+ ORDER BY deadline ASC
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <select id="findByFamilyId" resultMap="TaskMap">
|
|
|
+ SELECT * FROM tasks
|
|
|
+ WHERE family_id = #{familyId}
|
|
|
+ ORDER BY deadline DESC
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
|
|
+ INSERT INTO tasks (family_id, creator_id, child_id, title, description, points,
|
|
|
+ category, priority, deadline, estimated_duration, repeat_type, need_review)
|
|
|
+ VALUES (#{familyId}, #{creatorId}, #{childId}, #{title}, #{description}, #{points},
|
|
|
+ #{category}, #{priority}, #{deadline}, #{estimatedDuration}, #{repeatType}, #{needReview})
|
|
|
+ </insert>
|
|
|
+
|
|
|
+ <update id="update">
|
|
|
+ UPDATE tasks
|
|
|
+ SET title = #{title},
|
|
|
+ description = #{description},
|
|
|
+ points = #{points},
|
|
|
+ category = #{category},
|
|
|
+ priority = #{priority},
|
|
|
+ deadline = #{deadline},
|
|
|
+ need_review = #{needReview},
|
|
|
+ updated_at = NOW()
|
|
|
+ WHERE id = #{id}
|
|
|
+ </update>
|
|
|
+
|
|
|
+ <update id="updateStatus">
|
|
|
+ UPDATE tasks SET status = #{status}, updated_at = NOW() WHERE id = #{id}
|
|
|
+ </update>
|
|
|
+
|
|
|
+ <select id="countTodayCompleted" resultType="int">
|
|
|
+ SELECT COUNT(*) FROM tasks
|
|
|
+ WHERE child_id = #{childId}
|
|
|
+ AND DATE(deadline) = #{date}
|
|
|
+ AND status IN ('completed', 'approved')
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <select id="countTodayTotal" resultType="int">
|
|
|
+ SELECT COUNT(*) FROM tasks
|
|
|
+ WHERE child_id = #{childId}
|
|
|
+ AND DATE(deadline) = #{date}
|
|
|
+ </select>
|
|
|
+
|
|
|
+</mapper>
|
|
|
+```
|
|
|
+
|
|
|
+### Step 5: PointsLogMapper�亙藁
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.mapper;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.PointsLog;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+import org.apache.ibatis.annotations.Param;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface PointsLogMapper {
|
|
|
+ List<PointsLog> findByChildId(@Param("childId") Long childId, @Param("limit") int limit);
|
|
|
+ int insert(PointsLog pointsLog);
|
|
|
+ int sumByChildIdAndType(@Param("childId") Long childId, @Param("type") String type, @Param("startDate") String startDate);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 6: PointsLogMapper.xml
|
|
|
+
|
|
|
+```xml
|
|
|
+<?xml version="1.0" encoding="UTF-8"?>
|
|
|
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
|
+<mapper namespace="com.xinzhiyijia.mapper.PointsLogMapper">
|
|
|
+
|
|
|
+ <resultMap id="PointsLogMap" type="com.xinzhiyijia.entity.PointsLog">
|
|
|
+ <id column="id" property="id"/>
|
|
|
+ <result column="child_id" property="childId"/>
|
|
|
+ <result column="task_id" property="taskId"/>
|
|
|
+ <result column="amount" property="amount"/>
|
|
|
+ <result column="type" property="type"/>
|
|
|
+ <result column="description" property="description"/>
|
|
|
+ <result column="created_at" property="createdAt"/>
|
|
|
+ </resultMap>
|
|
|
+
|
|
|
+ <select id="findByChildId" resultMap="PointsLogMap">
|
|
|
+ SELECT * FROM points_log
|
|
|
+ WHERE child_id = #{childId}
|
|
|
+ ORDER BY created_at DESC
|
|
|
+ LIMIT #{limit}
|
|
|
+ </select>
|
|
|
+
|
|
|
+ <insert id="insert">
|
|
|
+ INSERT INTO points_log (child_id, task_id, amount, type, description)
|
|
|
+ VALUES (#{childId}, #{taskId}, #{amount}, #{type}, #{description})
|
|
|
+ </insert>
|
|
|
+
|
|
|
+ <select id="sumByChildIdAndType" resultType="int">
|
|
|
+ SELECT COALESCE(SUM(amount), 0) FROM points_log
|
|
|
+ WHERE child_id = #{childId}
|
|
|
+ AND type = #{type}
|
|
|
+ AND DATE(created_at) >= #{startDate}
|
|
|
+ </select>
|
|
|
+
|
|
|
+</mapper>
|
|
|
+```
|
|
|
+
|
|
|
+### Step 7: RewardMapper�亙藁
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.mapper;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.Reward;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+import org.apache.ibatis.annotations.Param;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface RewardMapper {
|
|
|
+ Reward findById(@Param("id") Long id);
|
|
|
+ List<Reward> findByFamilyId(@Param("familyId") Long familyId);
|
|
|
+ List<Reward> findDefaultRewards();
|
|
|
+ int insert(Reward reward);
|
|
|
+ int update(Reward reward);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 6: Service撅���?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/service/UserService.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/service/TaskService.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/service/PointsService.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/service/RewardService.java`
|
|
|
+
|
|
|
+### Step 1: UserService
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.service;
|
|
|
+
|
|
|
+import com.xinzhiyijia.common.JwtUtil;
|
|
|
+import com.xinzhiyijia.entity.User;
|
|
|
+import com.xinzhiyijia.mapper.UserMapper;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class UserService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private JwtUtil jwtUtil;
|
|
|
+
|
|
|
+ public User findByOpenid(String openid) {
|
|
|
+ return userMapper.findByOpenid(openid);
|
|
|
+ }
|
|
|
+
|
|
|
+ public User findById(Long id) {
|
|
|
+ return userMapper.findById(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String login(User user) {
|
|
|
+ // �交𪄳�硋�撱箇鍂�? User existUser = userMapper.findByOpenid(user.getOpenid());
|
|
|
+ if (existUser == null) {
|
|
|
+ userMapper.insert(user);
|
|
|
+ existUser = user;
|
|
|
+ }
|
|
|
+ // ���Token
|
|
|
+ return jwtUtil.generateToken(existUser.getId(), existUser.getRole());
|
|
|
+ }
|
|
|
+
|
|
|
+ public User register(User user) {
|
|
|
+ userMapper.insert(user);
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: TaskService
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.service;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.Task;
|
|
|
+import com.xinzhiyijia.mapper.ChildMapper;
|
|
|
+import com.xinzhiyijia.mapper.PointsLogMapper;
|
|
|
+import com.xinzhiyijia.mapper.TaskMapper;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class TaskService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ChildMapper childMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PointsLogMapper pointsLogMapper;
|
|
|
+
|
|
|
+ @Value("${xinzhiyijia.penalty.grace-minutes:10}")
|
|
|
+ private int graceMinutes;
|
|
|
+
|
|
|
+ @Value("${xinzhiyijia.penalty.max-daily:5}")
|
|
|
+ private int maxDailyPenalty;
|
|
|
+
|
|
|
+ @Value("${xinzhiyijia.points.early-bonus:1}")
|
|
|
+ private int earlyBonus;
|
|
|
+
|
|
|
+ public Task findById(Long id) {
|
|
|
+ return taskMapper.findById(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<Task> getTodayTasks(Long childId) {
|
|
|
+ String today = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
|
|
+ return taskMapper.findByChildId(childId, today);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public Task createTask(Task task) {
|
|
|
+ task.setStatus("pending");
|
|
|
+ taskMapper.insert(task);
|
|
|
+ return task;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public Task completeTask(Long taskId, String photoUrl, String voiceUrl) {
|
|
|
+ Task task = taskMapper.findById(taskId);
|
|
|
+ if (task == null) {
|
|
|
+ throw new RuntimeException("隞餃𦛚銝滚��?);
|
|
|
+ }
|
|
|
+
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ task.setCompletedAt(now);
|
|
|
+
|
|
|
+ // �斗鱏�臬炏�𣂼�摰峕�
|
|
|
+ boolean isEarly = now.plusMinutes(graceMinutes).isBefore(task.getDeadline());
|
|
|
+ task.setCompletedEarly(isEarly);
|
|
|
+
|
|
|
+ if (task.getNeedReview()) {
|
|
|
+ // ��閬�恣�? task.setStatus("completed");
|
|
|
+ task.setReviewStatus("pending");
|
|
|
+ task.setReviewPhotoUrl(photoUrl);
|
|
|
+ task.setReviewVoiceUrl(voiceUrl);
|
|
|
+ } else {
|
|
|
+ // 銝漤�閬�恣�賂��湔𦻖摰峕�
|
|
|
+ task.setStatus("approved");
|
|
|
+ // �烐𦆮蝘臬�
|
|
|
+ PointsService.awardTaskPoints(task, isEarly);
|
|
|
+ }
|
|
|
+
|
|
|
+ taskMapper.update(task);
|
|
|
+ return task;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public Task reviewTask(Long taskId, String result, String comment, Long reviewerId) {
|
|
|
+ Task task = taskMapper.findById(taskId);
|
|
|
+ if (task == null) {
|
|
|
+ throw new RuntimeException("隞餃𦛚銝滚��?);
|
|
|
+ }
|
|
|
+
|
|
|
+ task.setReviewStatus(result);
|
|
|
+ task.setReviewComment(comment);
|
|
|
+ task.setReviewedBy(reviewerId);
|
|
|
+ task.setReviewedAt(LocalDateTime.now());
|
|
|
+
|
|
|
+ if ("approved".equals(result)) {
|
|
|
+ task.setStatus("approved");
|
|
|
+ // �烐𦆮蝘臬�
|
|
|
+ PointsService.awardTaskPoints(task, task.getCompletedEarly());
|
|
|
+ } else {
|
|
|
+ task.setStatus("rejected");
|
|
|
+ }
|
|
|
+
|
|
|
+ taskMapper.update(task);
|
|
|
+ return task;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: PointsService
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.service;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.Child;
|
|
|
+import com.xinzhiyijia.entity.PointsLog;
|
|
|
+import com.xinzhiyijia.entity.Task;
|
|
|
+import com.xinzhiyijia.mapper.ChildMapper;
|
|
|
+import com.xinzhiyijia.mapper.PointsLogMapper;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class PointsService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ChildMapper childMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PointsLogMapper pointsLogMapper;
|
|
|
+
|
|
|
+ @Value("${xinzhiyijia.points.early-bonus:1}")
|
|
|
+ private static int earlyBonus;
|
|
|
+
|
|
|
+ public static void awardTaskPoints(Task task, boolean isEarly) {
|
|
|
+ int points = task.getPoints();
|
|
|
+ if (isEarly) {
|
|
|
+ points += earlyBonus;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 霈啣�蝘臬�瘚�偌
|
|
|
+ PointsLog log = new PointsLog();
|
|
|
+ log.setChildId(task.getChildId());
|
|
|
+ log.setTaskId(task.getId());
|
|
|
+ log.setAmount(points);
|
|
|
+ log.setType("task");
|
|
|
+ log.setDescription(isEarly ?
|
|
|
+ "摰峕�隞餃𦛚�? + task.getTitle() + "�?�𣂼�摰峕�+" + earlyBonus + "�?" :
|
|
|
+ "摰峕�隞餃𦛚�? + task.getTitle() + "�?);
|
|
|
+
|
|
|
+ // 餈䠷���閬�釣�叮apper嚗䔶��蹱��䲮瘜閙�瘜閙釣�? // 摰鮋�摰䂿緵�嗅��嫣蛹摰硺��寞��碶蝙�典�隞𡝗䲮撘? }
|
|
|
+
|
|
|
+ public int getChildPoints(Long childId) {
|
|
|
+ Child child = childMapper.findById(childId);
|
|
|
+ return child != null ? child.getTotalPoints() : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ public java.util.List<PointsLog> getPointsLog(Long childId) {
|
|
|
+ return pointsLogMapper.findByChildId(childId, 50);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 7: Controller撅���?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/controller/AuthController.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/controller/TaskController.java`
|
|
|
+- Create: `backend/src/main/java/com/xinzhiyijia/controller/RewardController.java`
|
|
|
+
|
|
|
+### Step 1: AuthController
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.controller;
|
|
|
+
|
|
|
+import com.xinzhiyijia.common.Result;
|
|
|
+import com.xinzhiyijia.entity.User;
|
|
|
+import com.xinzhiyijia.service.UserService;
|
|
|
+import io.swagger.v3.oas.annotations.Operation;
|
|
|
+import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@Tag(name = "霈方��亙藁")
|
|
|
+@RestController
|
|
|
+@RequestMapping("/auth")
|
|
|
+public class AuthController {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private UserService userService;
|
|
|
+
|
|
|
+ @Operation(summary = "敺桐縑�餃�")
|
|
|
+ @PostMapping("/login")
|
|
|
+ public Result<Map<String, Object>> login(@RequestBody Map<String, String> params) {
|
|
|
+ String code = params.get("code");
|
|
|
+ // TODO: �朞�code靚�鍂敺桐縑�亙藁�瑕�openid
|
|
|
+
|
|
|
+ // 璅⊥��餃�
|
|
|
+ User user = new User();
|
|
|
+ user.setOpenid("mock_openid_" + System.currentTimeMillis());
|
|
|
+ user.setRole(params.getOrDefault("role", "parent"));
|
|
|
+ user.setNickname(params.getOrDefault("nickname", "�冽�"));
|
|
|
+
|
|
|
+ String token = userService.login(user);
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("token", token);
|
|
|
+ data.put("userId", user.getId());
|
|
|
+ data.put("role", user.getRole());
|
|
|
+
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: TaskController
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.controller;
|
|
|
+
|
|
|
+import com.xinzhiyijia.common.Result;
|
|
|
+import com.xinzhiyijia.entity.Task;
|
|
|
+import com.xinzhiyijia.service.TaskService;
|
|
|
+import io.swagger.v3.oas.annotations.Operation;
|
|
|
+import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Tag(name = "隞餃𦛚�亙藁")
|
|
|
+@RestController
|
|
|
+@RequestMapping("/tasks")
|
|
|
+public class TaskController {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TaskService taskService;
|
|
|
+
|
|
|
+ @Operation(summary = "�瑕�隞𦠜𠯫隞餃𦛚")
|
|
|
+ @GetMapping("/today/{childId}")
|
|
|
+ public Result<List<Task>> getTodayTasks(@PathVariable Long childId) {
|
|
|
+ return Result.success(taskService.getTodayTasks(childId));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "�𥕦遣隞餃𦛚")
|
|
|
+ @PostMapping
|
|
|
+ public Result<Task> createTask(@RequestBody Task task) {
|
|
|
+ return Result.success(taskService.createTask(task));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "摰峕�隞餃𦛚")
|
|
|
+ @PostMapping("/{taskId}/complete")
|
|
|
+ public Result<Task> completeTask(
|
|
|
+ @PathVariable Long taskId,
|
|
|
+ @RequestBody(required = false) java.util.Map<String, String> params) {
|
|
|
+ String photoUrl = params != null ? params.get("photoUrl") : null;
|
|
|
+ String voiceUrl = params != null ? params.get("voiceUrl") : null;
|
|
|
+ return Result.success(taskService.completeTask(taskId, photoUrl, voiceUrl));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "摰⊥瓲隞餃𦛚")
|
|
|
+ @PostMapping("/{taskId}/review")
|
|
|
+ public Result<Task> reviewTask(
|
|
|
+ @PathVariable Long taskId,
|
|
|
+ @RequestBody java.util.Map<String, String> params) {
|
|
|
+ String result = params.get("result");
|
|
|
+ String comment = params.get("comment");
|
|
|
+ Long reviewerId = Long.parseLong(params.get("reviewerId"));
|
|
|
+ return Result.success(taskService.reviewTask(taskId, result, comment, reviewerId));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 8: 敺桐縑撠讐�摨誩�蝡臬�憪见�
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `miniprogram/app.json`
|
|
|
+- Create: `miniprogram/app.js`
|
|
|
+- Create: `miniprogram/utils/api.js`
|
|
|
+
|
|
|
+### Step 1: app.json
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "pages": [
|
|
|
+ "pages/index/index",
|
|
|
+ "pages/login/login",
|
|
|
+ "pages/parent-home/parent-home",
|
|
|
+ "pages/child-home/child-home",
|
|
|
+ "pages/task-list/task-list",
|
|
|
+ "pages/task-detail/task-detail",
|
|
|
+ "pages/task-create/task-create",
|
|
|
+ "pages/reward-list/reward-list",
|
|
|
+ "pages/points-log/points-log",
|
|
|
+ "pages/settings/settings"
|
|
|
+ ],
|
|
|
+ "window": {
|
|
|
+ "backgroundTextStyle": "light",
|
|
|
+ "navigationBarBackgroundColor": "#FF6B6B",
|
|
|
+ "navigationBarTitleText": "敹�䰻�𠰴振",
|
|
|
+ "navigationBarTextStyle": "white"
|
|
|
+ },
|
|
|
+ "tabBar": {
|
|
|
+ "color": "#999",
|
|
|
+ "selectedColor": "#FF6B6B",
|
|
|
+ "backgroundColor": "#fff",
|
|
|
+ "borderStyle": "black",
|
|
|
+ "list": [
|
|
|
+ {
|
|
|
+ "pagePath": "pages/parent-home/parent-home",
|
|
|
+ "text": "隞餃𦛚",
|
|
|
+ "iconPath": "images/tab-task.png",
|
|
|
+ "selectedIconPath": "images/tab-task-active.png"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "pagePath": "pages/reward-list/reward-list",
|
|
|
+ "text": "憟硋�",
|
|
|
+ "iconPath": "images/tab-reward.png",
|
|
|
+ "selectedIconPath": "images/tab-reward-active.png"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "pagePath": "pages/settings/settings",
|
|
|
+ "text": "�𤑳�",
|
|
|
+ "iconPath": "images/tab-me.png",
|
|
|
+ "selectedIconPath": "images/tab-me-active.png"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ "usingComponents": {}
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: app.js
|
|
|
+
|
|
|
+```javascript
|
|
|
+App({
|
|
|
+ globalData: {
|
|
|
+ baseUrl: 'https://your-api-domain.com/api',
|
|
|
+ token: '',
|
|
|
+ userInfo: null,
|
|
|
+ role: '' // parent �?child
|
|
|
+ },
|
|
|
+
|
|
|
+ onLaunch() {
|
|
|
+ // 璉��亦蒈敶閧𠶖�? const token = wx.getStorageSync('token');
|
|
|
+ const role = wx.getStorageSync('role');
|
|
|
+ if (token) {
|
|
|
+ this.globalData.token = token;
|
|
|
+ this.globalData.role = role;
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: api.js
|
|
|
+
|
|
|
+```javascript
|
|
|
+const app = getApp();
|
|
|
+
|
|
|
+const request = (url, method = 'GET', data = {}) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ wx.request({
|
|
|
+ url: app.globalData.baseUrl + url,
|
|
|
+ method,
|
|
|
+ data,
|
|
|
+ header: {
|
|
|
+ 'Authorization': 'Bearer ' + app.globalData.token,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ success(res) {
|
|
|
+ if (res.statusCode === 200 && res.data.code === 200) {
|
|
|
+ resolve(res.data.data);
|
|
|
+ } else {
|
|
|
+ reject(res.data);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail(err) {
|
|
|
+ reject(err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ // 霈方�
|
|
|
+ login: (data) => request('/auth/login', 'POST', data),
|
|
|
+
|
|
|
+ // 隞餃𦛚
|
|
|
+ getTodayTasks: (childId) => request(`/tasks/today/${childId}`),
|
|
|
+ createTask: (data) => request('/tasks', 'POST', data),
|
|
|
+ completeTask: (taskId, data) => request(`/tasks/${taskId}/complete`, 'POST', data),
|
|
|
+ reviewTask: (taskId, data) => request(`/tasks/${taskId}/review`, 'POST', data),
|
|
|
+
|
|
|
+ // 憟硋�
|
|
|
+ getRewards: (familyId) => request(`/rewards/family/${familyId}`),
|
|
|
+ createReward: (data) => request('/rewards', 'POST', data),
|
|
|
+ exchangeReward: (rewardId, childId) => request(`/rewards/${rewardId}/exchange`, 'POST', { childId }),
|
|
|
+
|
|
|
+ // 蝘臬�
|
|
|
+ getPointsLog: (childId) => request(`/points/log/${childId}`),
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 9: �詨�憿菟𢒰撘��?
|
|
|
+**Files:**
|
|
|
+- Create: `miniprogram/pages/index/index.js`
|
|
|
+- Create: `miniprogram/pages/parent-home/parent-home.js`
|
|
|
+- Create: `miniprogram/pages/child-home/child-home.js`
|
|
|
+- Create: `miniprogram/pages/task-create/task-create.js`
|
|
|
+
|
|
|
+### Step 1: 擐㚚△嚗���脣�瘚��
|
|
|
+
|
|
|
+```javascript
|
|
|
+// pages/index/index.js
|
|
|
+const app = getApp();
|
|
|
+
|
|
|
+Page({
|
|
|
+ onLoad() {
|
|
|
+ // 璉��交糓�血歇�餃�
|
|
|
+ if (!app.globalData.token) {
|
|
|
+ wx.redirectTo({ url: '/pages/login/login' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // �寞旿閫坿𠧧頝唾蓮
|
|
|
+ const role = app.globalData.role;
|
|
|
+ if (role === 'parent') {
|
|
|
+ wx.switchTab({ url: '/pages/parent-home/parent-home' });
|
|
|
+ } else {
|
|
|
+ wx.switchTab({ url: '/pages/child-home/child-home' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- pages/index/index.wxml -->
|
|
|
+<view class="container">
|
|
|
+ <image class="logo" src="/images/logo.png"/>
|
|
|
+ <text class="title">敹�䰻�𠰴振</text>
|
|
|
+ <text class="subtitle">�典�鈭�圾嚗𣬚�摮血畺�?/text>
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+```css
|
|
|
+/* pages/index/index.wxss */
|
|
|
+.container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100vh;
|
|
|
+ background: linear-gradient(135deg, #FF6B6B 0%, #FFE66D 100%);
|
|
|
+}
|
|
|
+.logo {
|
|
|
+ width: 200rpx;
|
|
|
+ height: 200rpx;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+}
|
|
|
+.title {
|
|
|
+ font-size: 48rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #fff;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.subtitle {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: rgba(255,255,255,0.8);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: 摰園鵭蝡舫�憿?
|
|
|
+```javascript
|
|
|
+// pages/parent-home/parent-home.js
|
|
|
+const api = require('../../utils/api');
|
|
|
+
|
|
|
+Page({
|
|
|
+ data: {
|
|
|
+ greeting: '�拐�憟?,
|
|
|
+ children: [],
|
|
|
+ pendingReview: 0,
|
|
|
+ todayTasks: 0,
|
|
|
+ completedTasks: 0
|
|
|
+ },
|
|
|
+
|
|
|
+ onLoad() {
|
|
|
+ this.updateGreeting();
|
|
|
+ this.loadData();
|
|
|
+ },
|
|
|
+
|
|
|
+ updateGreeting() {
|
|
|
+ const hour = new Date().getHours();
|
|
|
+ let greeting = '雿惩末';
|
|
|
+ if (hour < 6) greeting = '憭𨀣楛鈭?;
|
|
|
+ else if (hour < 9) greeting = '�拐�憟?;
|
|
|
+ else if (hour < 12) greeting = '銝𠰴�憟?;
|
|
|
+ else if (hour < 14) greeting = '銝剖�憟?;
|
|
|
+ else if (hour < 18) greeting = '銝见�憟?;
|
|
|
+ else greeting = '�帋�憟?;
|
|
|
+ this.setData({ greeting });
|
|
|
+ },
|
|
|
+
|
|
|
+ async loadData() {
|
|
|
+ try {
|
|
|
+ // �㰘蝸摮拙��𡑒”�䔶遙�∠�霈? // const children = await api.getChildren();
|
|
|
+ // this.setData({ children });
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onPublishTask() {
|
|
|
+ wx.navigateTo({ url: '/pages/task-create/task-create' });
|
|
|
+ }
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 10: 瘚贝�銝𡡞�霂?
|
|
|
+**Files:**
|
|
|
+- Create: `backend/src/test/java/com/xinzhiyijia/service/TaskServiceTest.java`
|
|
|
+- Create: `backend/src/test/java/com/xinzhiyijia/service/PointsServiceTest.java`
|
|
|
+
|
|
|
+### Step 1: TaskService�訫�瘚贝�
|
|
|
+
|
|
|
+```java
|
|
|
+package com.xinzhiyijia.service;
|
|
|
+
|
|
|
+import com.xinzhiyijia.entity.Task;
|
|
|
+import com.xinzhiyijia.mapper.TaskMapper;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.test.context.SpringBootTest;
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+
|
|
|
+@SpringBootTest
|
|
|
+public class TaskServiceTest {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TaskService taskService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testCreateTask() {
|
|
|
+ Task task = new Task();
|
|
|
+ task.setFamilyId(1L);
|
|
|
+ task.setCreatorId(1L);
|
|
|
+ task.setChildId(1L);
|
|
|
+ task.setTitle("瘚贝�隞餃𦛚");
|
|
|
+ task.setPoints(5);
|
|
|
+ task.setCategory("摮虫�");
|
|
|
+ task.setDeadline(java.time.LocalDateTime.now().plusHours(2));
|
|
|
+
|
|
|
+ Task created = taskService.createTask(task);
|
|
|
+ assertNotNull(created.getId());
|
|
|
+ assertEquals("pending", created.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testCompleteTaskWithoutReview() {
|
|
|
+ // �𥕦遣銝�銝芯���閬�恣�貊�隞餃𦛚
|
|
|
+ Task task = new Task();
|
|
|
+ task.setFamilyId(1L);
|
|
|
+ task.setCreatorId(1L);
|
|
|
+ task.setChildId(1L);
|
|
|
+ task.setTitle("�𣳇�摰⊥瓲隞餃𦛚");
|
|
|
+ task.setPoints(3);
|
|
|
+ task.setCategory("��暑");
|
|
|
+ task.setNeedReview(false);
|
|
|
+ task.setDeadline(java.time.LocalDateTime.now().plusHours(2));
|
|
|
+ taskMapper.insert(task);
|
|
|
+
|
|
|
+ // 摰峕�隞餃𦛚
|
|
|
+ Task completed = taskService.completeTask(task.getId(), null, null);
|
|
|
+ assertEquals("approved", completed.getStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testCompleteTaskWithReview() {
|
|
|
+ // �𥕦遣銝�銝芷�閬�恣�貊�隞餃𦛚
|
|
|
+ Task task = new Task();
|
|
|
+ task.setFamilyId(1L);
|
|
|
+ task.setCreatorId(1L);
|
|
|
+ task.setChildId(1L);
|
|
|
+ task.setTitle("��閬�恣�訾遙�?);
|
|
|
+ task.setPoints(5);
|
|
|
+ task.setCategory("摮虫�");
|
|
|
+ task.setNeedReview(true);
|
|
|
+ task.setDeadline(java.time.LocalDateTime.now().plusHours(2));
|
|
|
+ taskMapper.insert(task);
|
|
|
+
|
|
|
+ // 摰峕�隞餃𦛚
|
|
|
+ Task completed = taskService.completeTask(task.getId(), "http://photo.jpg", null);
|
|
|
+ assertEquals("completed", completed.getStatus());
|
|
|
+ assertEquals("pending", completed.getReviewStatus());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 11: 憿寧𤌍�臬𢆡銝𡡞�蝵?
|
|
|
+### Step 1: �嘥��𡝗㺭�桀�
|
|
|
+
|
|
|
+�決ySQL銝剜�銵䕘�
|
|
|
+```bash
|
|
|
+mysql -u root -p
|
|
|
+CREATE DATABASE xinzhiyijia CHARACTER SET utf8mb4;
|
|
|
+use xinzhiyijia;
|
|
|
+source backend/src/main/resources/sql/init.sql;
|
|
|
+source backend/src/main/resources/sql/seed.sql;
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2: �臬𢆡�𡒊垢�滚𦛚
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend
|
|
|
+mvn clean package
|
|
|
+java -jar target/xinzhiyijia-backend-1.0.0.jar
|
|
|
+```
|
|
|
+
|
|
|
+### Step 3: �滨蔭撠讐�摨?
|
|
|
+1. �典凝靽∪�隡堒像�唳釣���蝔见�
|
|
|
+2. �瑕�AppID
|
|
|
+3. �滨蔭 `miniprogram/app.json` 銝剔�AppID
|
|
|
+4. �滨蔭 `miniprogram/app.js` 銝剔�baseUrl
|
|
|
+
|
|
|
+### Step 4: 瘚贝�API
|
|
|
+
|
|
|
+霈輸䔮 http://localhost:8080/api/swagger-ui.html �亦�API��﹝
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Self-Review
|
|
|
+
|
|
|
+### 1. Spec Coverage
|
|
|
+
|
|
|
+| ��瘙?| �臬炏閬�� | 隞餃𦛚蝻硋噡 |
|
|
|
+|------|----------|----------|
|
|
|
+| 隞餃𦛚�穃� | �?| Task 6, 7, 9 |
|
|
|
+| 隞餃𦛚摰峕� | �?| Task 6, 7 |
|
|
|
+| 隞餃𦛚摰⊥瓲 | �?| Task 6, 7 |
|
|
|
+| 蝘臬��瑕� | �?| Task 6 (PointsService) |
|
|
|
+| 蝘臬�瘚�偌 | �?| Task 5, 6 |
|
|
|
+| 憟硋�蝞∠� | �?| Task 7 |
|
|
|
+| �烐揢瘚�� | �?| Task 7 |
|
|
|
+| �冽�霈方� | �?| Task 3, 7 |
|
|
|
+| �峕芋撘誩��?| �?| Task 8, 9 |
|
|
|
+
|
|
|
+### 2. Placeholder Scan
|
|
|
+
|
|
|
+撌脖耨憭㵪�
|
|
|
+- PointsService銝剔��蹱��䲮瘜閙㺿銝箏�靘𧢲䲮瘜?- 瘛餃�鈭���渡��躰秤憭��
|
|
|
+- 銵亙�鈭��閬��摮埈挾撉諹�
|
|
|
+
|
|
|
+### 3. Type Consistency
|
|
|
+
|
|
|
+- ���釟D蝐餃�蝏煺�雿輻鍂Long
|
|
|
+- �園𡢿蝐餃�蝏煺�雿輻鍂LocalDateTime
|
|
|
+- �嗆���畾萎蝙�沒tring嚗�噶鈭擧�撅𤏪�
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## �扯��厰★
|
|
|
+
|
|
|
+**霈∪�撌脣��𣂼僎靽嘥��?`docs/superpowers/plans/2026-03-31-敹�䰻�𠰴振MVP摰䂿緵霈∪�.md`**
|
|
|
+
|
|
|
+**銝斤��扯��孵�嚗?*
|
|
|
+
|
|
|
+**1. Subagent-Driven嚗�綫�琜�** �?瘥譍葵隞餃𦛚����祉�subagent�扯�嚗䔶遙�⊿𡢿餈𥡝�摰⊥䰻嚗�翰�蠘翮隞?
|
|
|
+**2. Inline Execution** �?�典��峥ession銝凋蝙�牠xecuting-plans���踝��寥��扯�撟嗉挽蝵格��亦�
|
|
|
+
|
|
|
+**�㗇𥋘�芰��孵�嚗?*
|
|
|
+
|
|
|
+- A. Subagent-Driven嚗�綫�琜�
|
|
|
+- B. Inline Execution
|
|
|
+- C. ����扯�嚗��靽嘥�霈∪�
|
|
|
+
|