docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
This commit is contained in:
+539
@@ -0,0 +1,539 @@
|
||||
// 账单接口
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"simple-memo/global"
|
||||
"simple-memo/models"
|
||||
"simple-memo/utils"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// -------------------------- 1. 定义 Handler 接口 --------------------------
|
||||
// BillHandler 账单模块接口定义
|
||||
type BillHandler interface {
|
||||
AddBill(c *gin.Context) // 添加账单
|
||||
EditBill(c *gin.Context) // 编辑账单
|
||||
BillList(c *gin.Context) // 账单列表
|
||||
DeleteBill(c *gin.Context) // 删除账单
|
||||
MonthStat(c *gin.Context) // 月度统计(mine.vue 使用)
|
||||
CateStat(c *gin.Context) // 分类统计
|
||||
ExportBill(c *gin.Context) // 导出账单
|
||||
AggregateStat(c *gin.Context) // 聚合统计
|
||||
}
|
||||
|
||||
// -------------------------- 2. 实现结构体 --------------------------
|
||||
type billHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBillHandler() BillHandler {
|
||||
return &billHandler{
|
||||
db: global.DB,
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------- 3. 请求结构体 --------------------------
|
||||
|
||||
// AddBillReq 添加账单
|
||||
type AddBillReq struct {
|
||||
Type int `json:"type"` // 1-支出 2-收入 3-转账
|
||||
Money float64 `json:"money"` // 金额
|
||||
Cate string `json:"cate"` // 分类
|
||||
Note string `json:"note"` // 备注
|
||||
Channel string `json:"channel"` // 渠道
|
||||
Date string `json:"date"` // 日期 YYYY-MM-DD
|
||||
FromAccount string `json:"from_account"`
|
||||
ToAccount string `json:"to_account"`
|
||||
}
|
||||
|
||||
// EditBillReq 编辑账单
|
||||
type EditBillReq struct {
|
||||
ID uint `json:"id"`
|
||||
Type int `json:"type"`
|
||||
Money float64 `json:"money"`
|
||||
Cate string `json:"cate"`
|
||||
Note string `json:"note"`
|
||||
Channel string `json:"channel"`
|
||||
Date string `json:"date"`
|
||||
FromAccount string `json:"from_account"`
|
||||
ToAccount string `json:"to_account"`
|
||||
}
|
||||
|
||||
// BillListReq 账单列表
|
||||
type BillListReq struct {
|
||||
Date string `json:"date"` // 日期 YYYY-MM-DD
|
||||
StartTime string `json:"start_time"` // 开始时间 YYYY-MM-DD
|
||||
EndTime string `json:"end_time"` // 结束时间 YYYY-MM-DD
|
||||
}
|
||||
|
||||
// DeleteBillReq 删除账单
|
||||
type DeleteBillReq struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
// MonthStatReq 月度统计
|
||||
type MonthStatReq struct {
|
||||
Month string `form:"month"` // YYYY-MM
|
||||
}
|
||||
|
||||
// AggregateStatReq 聚合统计请求
|
||||
type AggregateStatReq struct {
|
||||
StartDate string `json:"start_date" binding:"required"` // 开始日期
|
||||
EndDate string `json:"end_date" binding:"required"` // 结束日期
|
||||
Type int `json:"type"` // 类型筛选 1-支出 2-收入,不传则全部
|
||||
}
|
||||
|
||||
// -------------------------- 4. 接口实现 --------------------------
|
||||
|
||||
// GetBillListForCalendar 获取日历月份的账单列表(内部方法)
|
||||
func (h *billHandler) GetBillListForCalendar(c *gin.Context, year, month int) []interface{} {
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// 计算月份范围
|
||||
startDate := fmt.Sprintf("%d-%02d-01", year, month)
|
||||
_, lastDay := utils.GetMonthLastDay(year, month)
|
||||
endDate := fmt.Sprintf("%d-%02d-%02d", year, month, lastDay)
|
||||
|
||||
var list []models.Bill
|
||||
h.db.Model(&models.Bill{}).
|
||||
Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
|
||||
Order("date ASC, id ASC").
|
||||
Find(&list)
|
||||
|
||||
result := make([]interface{}, len(list))
|
||||
for i, v := range list {
|
||||
result[i] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AddBill 添加账单
|
||||
func (h *billHandler) AddBill(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
var req AddBillReq
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查每日限制(最多100个账单/天)
|
||||
today := time.Now().Format("2006-01-02")
|
||||
var todayCount int64
|
||||
if err := h.db.Model(&models.Bill{}).Where("user_id = ? AND date = ? AND deleted_at IS NULL", userID, today).Count(&todayCount).Error; err != nil {
|
||||
utils.Fail(c, "系统错误")
|
||||
return
|
||||
}
|
||||
if todayCount >= 100 {
|
||||
utils.Fail(c, "今日账单数量已达上限(100个)")
|
||||
return
|
||||
}
|
||||
|
||||
bill := models.Bill{
|
||||
UserID: userID,
|
||||
Type: req.Type,
|
||||
Money: req.Money,
|
||||
Cate: req.Cate,
|
||||
Note: utils.FilterSensitive(req.Note), // 敏感词过滤
|
||||
Channel: req.Channel,
|
||||
Date: req.Date,
|
||||
FromAccount: req.FromAccount,
|
||||
ToAccount: req.ToAccount,
|
||||
}
|
||||
|
||||
if err := h.db.Create(&bill).Error; err != nil {
|
||||
utils.Fail(c, "添加失败")
|
||||
return
|
||||
}
|
||||
|
||||
var growthResult any
|
||||
debugMode := os.Getenv("DEBUG_MODE")
|
||||
// ==================== 调试模式 ====================
|
||||
if debugMode == "true" {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
levelUp := true // 20%概率升级
|
||||
hasAchievement := true // 30%概率获得成就
|
||||
growthResult = MockGrowthRewards(levelUp, hasAchievement)
|
||||
} else {
|
||||
// 生产模式:真实调用
|
||||
growthHandler := NewGrowthHandler()
|
||||
growthResult = growthHandler.AddExpInternal(userID, "bill", fmt.Sprintf("%d", bill.ID), nil)
|
||||
}
|
||||
// ==================================================
|
||||
|
||||
utils.Ok(c, gin.H{
|
||||
"success": true,
|
||||
"growth": growthResult,
|
||||
"bill": bill,
|
||||
"debugMode": debugMode,
|
||||
"isDebug": debugMode == "true",
|
||||
})
|
||||
}
|
||||
|
||||
// EditBill 编辑账单
|
||||
func (h *billHandler) EditBill(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
var req EditBillReq
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"type": req.Type,
|
||||
"money": req.Money,
|
||||
"cate": req.Cate,
|
||||
"note": utils.FilterSensitive(req.Note), // 敏感词过滤
|
||||
"date": req.Date,
|
||||
"channel": req.Channel,
|
||||
"from_account": req.FromAccount,
|
||||
"to_account": req.ToAccount,
|
||||
}
|
||||
|
||||
err := h.db.Model(&models.Bill{}).
|
||||
Where("id = ? AND user_id = ?", req.ID, userID).
|
||||
Updates(data).Error
|
||||
|
||||
if err != nil {
|
||||
utils.Fail(c, "编辑失败")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Ok(c, nil)
|
||||
}
|
||||
|
||||
// BillList 获取账单列表
|
||||
func (h *billHandler) BillList(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
// date := c.DefaultQuery("date", time.Now().Format("2006-01-02"))
|
||||
|
||||
var req BillListReq
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
paramsJson, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
global.Logger.Warn("序列化请求参数失败",
|
||||
append(utils.LogContextFields(c), zap.Error(err))...,
|
||||
)
|
||||
} else {
|
||||
global.Logger.Info("【请求参数】",
|
||||
append(utils.LogContextFields(c),
|
||||
zap.String("req", string(paramsJson)),
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
// 定义时间格式化模板
|
||||
const dateLayout = "2006-01-02"
|
||||
var queryDate, queryStart, queryEnd string
|
||||
|
||||
now := time.Now()
|
||||
today := now.Format(dateLayout)
|
||||
|
||||
date := req.Date
|
||||
startTime := req.StartTime
|
||||
endTime := req.EndTime
|
||||
|
||||
switch {
|
||||
|
||||
case startTime != "" && endTime != "":
|
||||
if _, err := time.Parse(dateLayout, startTime); err != nil {
|
||||
utils.Fail(c, "start_time格式错误,需为:2006-01-02", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, err := time.Parse(dateLayout, endTime); err != nil {
|
||||
utils.Fail(c, "end_time格式错误,需为:2006-01-02", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// 校验结束时间不能早于开始时间
|
||||
start, _ := time.Parse(dateLayout, startTime)
|
||||
end, _ := time.Parse(dateLayout, endTime)
|
||||
if end.Before(start) {
|
||||
utils.Fail(c, "结束日期不能早于开始日期", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
queryStart = startTime
|
||||
queryEnd = endTime
|
||||
case date != "" && startTime == "" && endTime == "":
|
||||
queryDate = date
|
||||
default:
|
||||
// 默认查询今日
|
||||
queryDate = today
|
||||
}
|
||||
|
||||
var list []models.Bill
|
||||
tx := h.db.Model(&models.Bill{}).Where("user_id = ?", userID)
|
||||
// 根据参数类型构建时间查询条件
|
||||
if queryStart != "" && queryEnd != "" {
|
||||
// 时间范围查询:date BETWEEN start AND end
|
||||
tx = tx.Where("date BETWEEN ? AND ?", queryStart, queryEnd)
|
||||
} else {
|
||||
// 单日查询
|
||||
tx = tx.Where("date = ?", queryDate)
|
||||
}
|
||||
err = tx.Order("id DESC").Find(&list).Error
|
||||
|
||||
if err != nil {
|
||||
utils.Fail(c, "获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Ok(c, list)
|
||||
}
|
||||
|
||||
// DeleteBill 删除账单
|
||||
func (h *billHandler) DeleteBill(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
var req DeleteBillReq
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 查询账单信息(用于扣除积分)
|
||||
var bill models.Bill
|
||||
err := h.db.Where("id = ? AND user_id = ?", req.ID, userID).First(&bill).Error
|
||||
if err != nil {
|
||||
utils.Fail(c, "账单不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除账单
|
||||
err = h.db.Where("id = ? AND user_id = ?", req.ID, userID).
|
||||
Delete(&models.Bill{}).Error
|
||||
|
||||
if err != nil {
|
||||
utils.Fail(c, "删除失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除今天添加时获得的积分
|
||||
growthHandler := NewGrowthHandler()
|
||||
today := time.Now().Format("2006-01-02")
|
||||
deducted := growthHandler.DeductExpInternal(userID, "bill", fmt.Sprintf("%d", req.ID), today)
|
||||
|
||||
utils.Ok(c, gin.H{
|
||||
"deducted_exp": deducted,
|
||||
})
|
||||
}
|
||||
|
||||
// MonthStat 月度统计(mine.vue 核心接口)
|
||||
func (h *billHandler) MonthStat(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
var req MonthStatReq
|
||||
_ = c.ShouldBindQuery(&req)
|
||||
|
||||
now := time.Now()
|
||||
month := req.Month
|
||||
if month == "" {
|
||||
month = now.Format("2006-01")
|
||||
}
|
||||
|
||||
// 收入
|
||||
var income float64
|
||||
h.db.Model(&models.Bill{}).
|
||||
Where("user_id = ? AND type = 2 AND date LIKE ?", userID, month+"%").
|
||||
Select("COALESCE(SUM(money), 0)").Scan(&income)
|
||||
|
||||
// 支出
|
||||
var expend float64
|
||||
h.db.Model(&models.Bill{}).
|
||||
Where("user_id = ? AND type = 1 AND date LIKE ?", userID, month+"%").
|
||||
Select("COALESCE(SUM(money), 0)").Scan(&expend)
|
||||
|
||||
// 待办完成数(需要关联 todo 表)
|
||||
var totalTask int64
|
||||
var finishTask int64
|
||||
h.db.Model(&models.Todo{}).Where("user_id = ? AND date LIKE ? ", userID, month+"%").Count(&totalTask)
|
||||
h.db.Model(&models.Todo{}).Where("user_id = ? AND date LIKE ? AND done = 1", userID, month+"%").Count(&finishTask)
|
||||
|
||||
utils.Ok(c, gin.H{
|
||||
"income": income,
|
||||
"expend": expend,
|
||||
"totalTask": totalTask,
|
||||
"finishTask": finishTask,
|
||||
})
|
||||
}
|
||||
|
||||
// CateStat 分类统计
|
||||
func (h *billHandler) CateStat(c *gin.Context) {
|
||||
utils.Ok(c, []any{})
|
||||
}
|
||||
|
||||
// ExportBill 导出账单
|
||||
func (h *billHandler) ExportBill(c *gin.Context) {
|
||||
utils.Ok(c, "导出功能开发中")
|
||||
}
|
||||
|
||||
// AggregateStat 聚合统计
|
||||
func (h *billHandler) AggregateStat(c *gin.Context) {
|
||||
userID := c.GetUint("userID")
|
||||
var req AggregateStatReq
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Fail(c, "参数解析失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建基础查询
|
||||
baseQuery := h.db.Model(&models.Bill{}).
|
||||
Where("user_id = ? AND date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate)
|
||||
|
||||
// 类型筛选
|
||||
if req.Type > 0 {
|
||||
baseQuery = baseQuery.Where("type = ?", req.Type)
|
||||
}
|
||||
|
||||
// ========== 1. 总体统计 ==========
|
||||
var totalExpend, totalIncome float64
|
||||
h.db.Model(&models.Bill{}).
|
||||
Where("user_id = ? AND date BETWEEN ? AND ? AND type = 1", userID, req.StartDate, req.EndDate).
|
||||
Select("COALESCE(SUM(money), 0)").Scan(&totalExpend)
|
||||
h.db.Model(&models.Bill{}).
|
||||
Where("user_id = ? AND date BETWEEN ? AND ? AND type = 2", userID, req.StartDate, req.EndDate).
|
||||
Select("COALESCE(SUM(money), 0)").Scan(&totalIncome)
|
||||
|
||||
balance := totalIncome - totalExpend
|
||||
|
||||
// ========== 2. 预算数据 ==========
|
||||
// 获取月份(假设StartDate和EndDate在同一月)
|
||||
month := req.StartDate[:7] // YYYY-MM
|
||||
var budget models.Budget
|
||||
h.db.Where("user_id = ? AND month = ?", userID, month).First(&budget)
|
||||
|
||||
budgetData := gin.H{
|
||||
"amount": 0,
|
||||
"used": totalExpend,
|
||||
"remaining": 0,
|
||||
"progress": 0.0,
|
||||
}
|
||||
if budget.ID > 0 {
|
||||
budgetData = gin.H{
|
||||
"amount": budget.Amount,
|
||||
"used": totalExpend,
|
||||
"remaining": budget.Amount - totalExpend,
|
||||
"progress": totalExpend / budget.Amount * 100,
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. 按分类统计 ==========
|
||||
type CategoryStat struct {
|
||||
Cate string `json:"cate"`
|
||||
Label string `json:"label"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
Expend float64 `json:"expend"`
|
||||
Income float64 `json:"income"`
|
||||
ExpendRate float64 `json:"expend_rate"`
|
||||
IncomeRate float64 `json:"income_rate"`
|
||||
}
|
||||
|
||||
var categoryResults []struct {
|
||||
Cate string
|
||||
Expend float64
|
||||
Income float64
|
||||
}
|
||||
h.db.Model(&models.Bill{}).
|
||||
Select(`
|
||||
cate,
|
||||
COALESCE(SUM(CASE WHEN type = 1 THEN money ELSE 0 END), 0) as expend,
|
||||
COALESCE(SUM(CASE WHEN type = 2 THEN money ELSE 0 END), 0) as income
|
||||
`).
|
||||
Where("user_id = ? AND date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate).
|
||||
Group("cate").
|
||||
Scan(&categoryResults)
|
||||
|
||||
byCategory := []CategoryStat{}
|
||||
for _, r := range categoryResults {
|
||||
expendRate := 0.0
|
||||
incomeRate := 0.0
|
||||
if totalExpend > 0 {
|
||||
expendRate = r.Expend / totalExpend * 100
|
||||
}
|
||||
if totalIncome > 0 {
|
||||
incomeRate = r.Income / totalIncome * 100
|
||||
}
|
||||
|
||||
byCategory = append(byCategory, CategoryStat{
|
||||
Cate: r.Cate,
|
||||
Label: r.Cate, // 可以后续从配置中读取中文标签
|
||||
Icon: "💰",
|
||||
Color: "#ff6b6b",
|
||||
Expend: r.Expend,
|
||||
Income: r.Income,
|
||||
ExpendRate: expendRate,
|
||||
IncomeRate: incomeRate,
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 4. 按渠道统计 ==========
|
||||
var channelResults []struct {
|
||||
Channel string
|
||||
Amount float64
|
||||
}
|
||||
h.db.Model(&models.Bill{}).
|
||||
Select("channel, SUM(money) as amount").
|
||||
Where("user_id = ? AND date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate).
|
||||
Group("channel").
|
||||
Scan(&channelResults)
|
||||
|
||||
byChannel := []gin.H{}
|
||||
for _, r := range channelResults {
|
||||
byChannel = append(byChannel, gin.H{
|
||||
"channel": r.Channel,
|
||||
"label": r.Channel,
|
||||
"amount": r.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 5. 日趋势 ==========
|
||||
var dailyResults []struct {
|
||||
Date string
|
||||
Expend float64
|
||||
Income float64
|
||||
}
|
||||
h.db.Model(&models.Bill{}).
|
||||
Select(`
|
||||
date,
|
||||
COALESCE(SUM(CASE WHEN type = 1 THEN money ELSE 0 END), 0) as expend,
|
||||
COALESCE(SUM(CASE WHEN type = 2 THEN money ELSE 0 END), 0) as income
|
||||
`).
|
||||
Where("user_id = ? AND date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate).
|
||||
Group("date").
|
||||
Order("date ASC").
|
||||
Scan(&dailyResults)
|
||||
|
||||
dailyTrend := []gin.H{}
|
||||
for _, r := range dailyResults {
|
||||
dailyTrend = append(dailyTrend, gin.H{
|
||||
"date": r.Date,
|
||||
"expend": r.Expend,
|
||||
"income": r.Income,
|
||||
})
|
||||
}
|
||||
|
||||
utils.Ok(c, gin.H{
|
||||
"total_expend": totalExpend,
|
||||
"total_income": totalIncome,
|
||||
"balance": balance,
|
||||
"budget": budgetData,
|
||||
"by_category": byCategory,
|
||||
"by_channel": byChannel,
|
||||
"daily_trend": dailyTrend,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user