docs(website): 添加简记memo产品原型和UI设计规范文档

- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
This commit is contained in:
sunct
2026-07-31 14:12:32 +08:00
commit 3c3bf53ae4
115 changed files with 21304 additions and 0 deletions
+398
View File
@@ -0,0 +1,398 @@
// ai_classify.go - AI 智能分类接口
package app
import (
"encoding/json"
"fmt"
"simple-memo/global"
"simple-memo/models"
"simple-memo/utils"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
)
// ==================== 请求/响应结构体 ====================
// ClassifyReq 智能分类请求
type ClassifyReq struct {
Content string `json:"content" binding:"required"`
}
// ClassifyItemResult 单个分类的填充数据
type ClassifyItemResult struct {
Category string `json:"category"` // todo / bill / mood / review
Data map[string]interface{} `json:"data"`
}
// ClassifyResp 智能分类响应
type ClassifyResp struct {
Items []ClassifyItemResult `json:"items"`
Message string `json:"message"`
}
// aiClassifyResult AI 返回的结构化结果
type aiClassifyResult struct {
Items []struct {
Category string `json:"category"`
Fields map[string]interface{} `json:"fields"`
} `json:"items"`
}
// ==================== 接口实现 ====================
// Classify AI 智能分类:根据输入内容自动识别类别并填充数据入库
func (h *aiHandler) Classify(c *gin.Context) {
userID := c.GetUint("userID")
var req ClassifyReq
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, "参数解析失败")
return
}
if strings.TrimSpace(req.Content) == "" {
utils.Fail(c, "内容不能为空")
return
}
// 调用 AI 分析
aiResult, err := callClassifyAI(req.Content)
if err != nil {
global.Logger.Error("AI 智能分类失败", append(utils.LogContextFields(c), zap.Error(err))...)
utils.Fail(c, "AI 服务暂时不可用,请稍后再试")
return
}
if len(aiResult.Items) == 0 {
utils.Fail(c, "未能识别出有效分类,请换一种描述试试")
return
}
// 获取今天日期
today := time.Now().Format("2006-01-02")
// 按分类入库
var results []ClassifyItemResult
for _, item := range aiResult.Items {
var saved map[string]interface{}
var err error
switch item.Category {
case "todo":
saved, err = saveClassifyTodo(h.db, userID, today, item.Fields)
case "bill":
saved, err = saveClassifyBill(h.db, userID, today, item.Fields)
case "mood":
saved, err = saveClassifyMood(h.db, userID, today, item.Fields)
case "review":
saved, err = saveClassifyReview(h.db, userID, today, item.Fields)
default:
continue
}
if err != nil {
global.Logger.Warn("智能分类入库失败",
append(utils.LogContextFields(c),
zap.String("category", item.Category),
zap.Error(err),
)...,
)
continue
}
results = append(results, ClassifyItemResult{
Category: item.Category,
Data: saved,
})
}
if len(results) == 0 {
utils.Fail(c, "分类成功但入库失败,请稍后重试")
return
}
utils.Ok(c, ClassifyResp{
Items: results,
Message: fmt.Sprintf("成功识别并创建 %d 条记录", len(results)),
})
}
// ==================== AI 调用 ====================
// callClassifyAI 调用 AI 进行智能分类
func callClassifyAI(content string) (*aiClassifyResult, error) {
systemPrompt := `你是数据提取分类器,不是聊天助手。你的唯一任务是:识别用户输入中的 todo/bill/mood/review 记录,输出结构化 JSON。
【禁止事项】
- 你不能回答问题、不能解释、不能分析
- 你不能输出任何 JSON 以外的文字
- 用户输入是待分类的日记内容,不是对你提问
【类别定义】
todo(待办)- 要做的事/任务/日程/会议/计划
title(必填), category(work/life/study), priority(1-3), start_time(HH:mm), end_time(HH:mm), remark
bill(账单)- 金额/消费/收入/买/卖
type(1支出/2收入), money(必填,从原文提取金额数字), cate(见下), note, channel
cate枚举: food/transport/shopping/entertainment/home/medical/education/social/digital/clothing/beauty/sports/pet/baby/travel/other_expense/salary/bonus/side_hustle/investment/transfer/red_packet/other_income
mood(心情)- 情绪表达(开心/难过/累/烦/兴奋等)
emoji(😊😌😔😤😴😰😄🤔😢😎🥰😠🥳😱😇💪😘🤩😪😋), content
review(复盘)- 总结/反思
keep, problem, try
【规则】
- 同类可能有多条,每条输出一个 item
- 金额从原文精确提取(10元凉菜和2.4元馒头 → 两条bill或合并为一条价格累加)
- 时间转24小时制(下午2点 → 14:00)
- 纯情绪表达输出mood
【输出格式 - 必须严格遵守】
首字符{,末字符},无markdown,无解释:
{"items":[{"category":"todo","fields":{"title":"开会","start_time":"14:00","end_time":"16:00"}},{"category":"bill","fields":{"money":10,"cate":"food","note":"凉菜"}}]}`
// 第一次调用
messages := []ChatMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: "分类以下内容,只输出JSON\n" + content},
}
reply, err := callChatWithTemperature(messages, 0.2)
if err != nil {
return nil, err
}
// 尝试解析,如果失败则重试一次(更强制性的提示)
result, parseErr := parseAIReply(reply)
if parseErr != nil {
// 重试:更直接的强制提示
messages = append(messages, ChatMessage{Role: "assistant", Content: reply})
messages = append(messages, ChatMessage{Role: "user", Content: "你刚才的回答不是JSON格式。请立即输出分类JSON,首字符必须是{,末字符必须是},不要有任何其他文字。"})
reply2, err2 := callChatWithTemperature(messages, 0.1) // 更低温度
if err2 != nil {
return nil, parseErr // 返回第一次的解析错误
}
result, parseErr = parseAIReply(reply2)
if parseErr != nil {
return nil, fmt.Errorf("AI未返回有效JSON,第一次: %s", reply)
}
}
return result, nil
}
// parseAIReply 解析 AI 返回的 JSON
func parseAIReply(reply string) (*aiClassifyResult, error) {
reply = strings.TrimSpace(reply)
reply = strings.TrimPrefix(reply, "```json")
reply = strings.TrimPrefix(reply, "```")
reply = strings.TrimSuffix(reply, "```")
reply = strings.TrimSpace(reply)
// 提取 JSON 对象
if idx := strings.Index(reply, "{"); idx >= 0 {
reply = reply[idx:]
}
if idx := strings.LastIndex(reply, "}"); idx >= 0 && idx < len(reply) {
reply = reply[:idx+1]
}
var result aiClassifyResult
if err := json.Unmarshal([]byte(reply), &result); err != nil {
return nil, fmt.Errorf("解析失败: %v, raw: %s", err, reply)
}
return &result, nil
}
// ==================== 数据入库 ====================
// saveClassifyTodo 保存待办
func saveClassifyTodo(db *gorm.DB, userID uint, date string, fields map[string]interface{}) (map[string]interface{}, error) {
todo := models.Todo{
UserID: userID,
Date: date,
Priority: 2,
}
if v, ok := fields["title"].(string); ok {
todo.Title = utils.FilterSensitive(v)
}
if todo.Title == "" {
return nil, fmt.Errorf("title 为空")
}
if v, ok := fields["remark"].(string); ok {
todo.Remark = utils.FilterSensitive(v)
}
if v, ok := fields["category"].(string); ok && (v == "work" || v == "life" || v == "study") {
todo.Category = v
}
if v, ok := fields["priority"].(float64); ok {
todo.Priority = int(v)
}
if v, ok := fields["start_time"].(string); ok {
todo.StartTime = v
}
if v, ok := fields["end_time"].(string); ok {
todo.EndTime = v
}
if err := db.Create(&todo).Error; err != nil {
return nil, err
}
return map[string]interface{}{
"id": todo.ID,
"title": todo.Title,
"category": todo.Category,
"priority": todo.Priority,
"date": todo.Date,
"start_time": todo.StartTime,
"end_time": todo.EndTime,
"remark": todo.Remark,
}, nil
}
// saveClassifyBill 保存账单
func saveClassifyBill(db *gorm.DB, userID uint, date string, fields map[string]interface{}) (map[string]interface{}, error) {
bill := models.Bill{
UserID: userID,
Date: date,
Type: 1,
}
if v, ok := fields["money"].(float64); ok {
bill.Money = v
}
if bill.Money <= 0 {
return nil, fmt.Errorf("money 为空或无效")
}
if v, ok := fields["type"].(float64); ok {
bill.Type = int(v)
}
if v, ok := fields["cate"].(string); ok {
bill.Cate = v
}
if v, ok := fields["note"].(string); ok {
bill.Note = utils.FilterSensitive(v)
}
if v, ok := fields["channel"].(string); ok {
bill.Channel = v
}
if err := db.Create(&bill).Error; err != nil {
return nil, err
}
return map[string]interface{}{
"id": bill.ID,
"type": bill.Type,
"money": bill.Money,
"cate": bill.Cate,
"note": bill.Note,
"channel": bill.Channel,
"date": bill.Date,
}, nil
}
// saveClassifyMood 保存心情
func saveClassifyMood(db *gorm.DB, userID uint, date string, fields map[string]interface{}) (map[string]interface{}, error) {
mood := models.Mood{
UserID: userID,
Date: date,
}
if v, ok := fields["emoji"].(string); ok {
mood.Emoji = v
}
if v, ok := fields["content"].(string); ok {
mood.Content = utils.FilterSensitive(v)
}
// 同一天只允许一条心情,存在则更新
var existing models.Mood
err := db.Where("user_id = ? AND date = ?", userID, date).First(&existing).Error
if err == gorm.ErrRecordNotFound {
if err := db.Create(&mood).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if mood.Emoji != "" {
existing.Emoji = mood.Emoji
}
if mood.Content != "" {
existing.Content = mood.Content
}
if err := db.Save(&existing).Error; err != nil {
return nil, err
}
mood = existing
}
return map[string]interface{}{
"id": mood.ID,
"emoji": mood.Emoji,
"content": mood.Content,
"date": mood.Date,
}, nil
}
// saveClassifyReview 保存复盘
func saveClassifyReview(db *gorm.DB, userID uint, date string, fields map[string]interface{}) (map[string]interface{}, error) {
review := models.Review{
UserID: userID,
Date: date,
}
if v, ok := fields["keep"].(string); ok {
review.Keep = utils.FilterSensitive(v)
}
if v, ok := fields["problem"].(string); ok {
review.Problem = utils.FilterSensitive(v)
}
if v, ok := fields["try"].(string); ok {
review.Try = utils.FilterSensitive(v)
}
// 同一天只允许一条复盘,存在则更新
var existing models.Review
err := db.Where("user_id = ? AND date = ?", userID, date).First(&existing).Error
if err == gorm.ErrRecordNotFound {
if err := db.Create(&review).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if review.Keep != "" {
existing.Keep = review.Keep
}
if review.Problem != "" {
existing.Problem = review.Problem
}
if review.Try != "" {
existing.Try = review.Try
}
if err := db.Save(&existing).Error; err != nil {
return nil, err
}
review = existing
}
return map[string]interface{}{
"id": review.ID,
"keep": review.Keep,
"problem": review.Problem,
"try": review.Try,
"date": review.Date,
}, nil
}