已完成用户APP首期功能开发

交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
czl231
2026-09-13 00:57:32 +08:00
parent a0a4bb1218
commit 3ef33b531d
793 changed files with 40959 additions and 1204 deletions

View File

@@ -0,0 +1,105 @@
// 功能向当前用户返回本人设备的可追溯用气统计版本1.0.0。
package user
import (
"math"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
)
// GetUsageStatistics 严格限定当前归属设备,没有计量数据时返回明确不可用状态。
func GetUsageStatistics(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
identity := strings.TrimSpace(ctx.Query("device_identity"))
if identity == "" {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var device models.ProductInfo
if err := ownedProductQuery(account.ID).Where("identity = ? AND device_kind IN ?", identity, []string{"valve", "alarm"}).First(&device).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
period := strings.TrimSpace(ctx.DefaultQuery("period", "month"))
anchor := time.Now()
if raw := strings.TrimSpace(ctx.Query("anchor")); raw != "" {
parsed, err := time.Parse("2006-01-02", raw)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
anchor = parsed
}
start, end, valid := usagePeriod(period, anchor)
if !valid {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var rows []models.DevUsageStat
if err := impl.DBService.Where("product_info_id = ? AND stat_date >= ? AND stat_date < ? AND status = ?", device.ID, start, end, common.StatusEnable).
Order("stat_date").Find(&rows).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
details := gin.H{"available": len(rows) > 0, "device": gin.H{"identity": device.Identity, "name": device.Name, "code": device.Code},
"period": period, "period_start": start.Format("2006-01-02"), "period_end": end.AddDate(0, 0, -1).Format("2006-01-02"), "unit": "kg", "points": []gin.H{}}
if len(rows) == 0 {
details["unavailable_reason"] = "该设备尚无可用计量数据"
infra.Response.Success(ctx, details)
return
}
points := make([]gin.H, 0, len(rows))
total, breakfast, lunch, dinner := 0.0, 0.0, 0.0, 0.0
latest := rows[0]
for _, row := range rows {
total += row.UsageKg
breakfast += row.BreakfastKg
lunch += row.LunchKg
dinner += row.DinnerKg
points = append(points, gin.H{"date": row.StatDate.Format("2006-01-02"), "usage": roundUsage(row.UsageKg)})
if row.CalculatedAt.After(latest.CalculatedAt) {
latest = row
}
}
details["total_usage"] = roundUsage(total)
details["average_usage"] = roundUsage(total / float64(len(rows)))
details["composition"] = gin.H{"breakfast": roundUsage(breakfast), "lunch": roundUsage(lunch), "dinner": roundUsage(dinner)}
details["points"] = points
details["source"] = latest.Source
details["calc_version"] = latest.CalcVersion
details["updated_at"] = latest.CalculatedAt
infra.Response.Success(ctx, details)
}
// usagePeriod 返回左闭右开的统计时间范围。
func usagePeriod(period string, anchor time.Time) (time.Time, time.Time, bool) {
day := time.Date(anchor.Year(), anchor.Month(), anchor.Day(), 0, 0, 0, 0, anchor.Location())
switch period {
case "day":
return day, day.AddDate(0, 0, 1), true
case "week":
offset := (int(day.Weekday()) + 6) % 7
start := day.AddDate(0, 0, -offset)
return start, start.AddDate(0, 0, 7), true
case "month":
start := time.Date(day.Year(), day.Month(), 1, 0, 0, 0, 0, day.Location())
return start, start.AddDate(0, 1, 0), true
case "year":
start := time.Date(day.Year(), 1, 1, 0, 0, 0, 0, day.Location())
return start, start.AddDate(1, 0, 0), true
default:
return time.Time{}, time.Time{}, false
}
}
func roundUsage(value float64) float64 { return math.Round(value*1000) / 1000 }