refactor: 五层对齐 22 表 + 工具并入 agent/ + 支付适配器内联(路由零变更)
- service/dto/controller 各拆 18 文件,一表一文件;跨表编排留在 outfit_generation_task_service - controller 共享 struct(outfit/member)保 /outfit/*、/member/* 前缀,路由零漂移(28 路径 diff 为空) - 修复 4 个缺失 g.Meta handler:/user/profile、/avatar/get、/body-measurement/get、/hairstyle/list 从 ALL 收敛为 GET - 工具包并入 agent/ 单包(weather/imagegen/avatar/scoring),NewCache 泛化 NewTTLCache - 虎皮椒支付适配器内联 service/payment_order_service.go,member 服务拆 4 表文件 - 冒烟 21 路径全绿 + 客户端 dart analyze 零 error
This commit is contained in:
@@ -22,7 +22,7 @@ func (c *avatar) Build(ctx context.Context, req *dto.AvatarBuildReq) (res *dto.A
|
||||
return &dto.AvatarBuildRes{AvatarId: a.Id, Status: a.BuildStatus}, nil
|
||||
}
|
||||
|
||||
func (c *avatar) Get(ctx context.Context, req *struct{}) (res *dto.AvatarGetRes, err error) {
|
||||
func (c *avatar) Get(ctx context.Context, req *dto.AvatarGetReq) (res *dto.AvatarGetRes, err error) {
|
||||
a, err := service.AvatarService.Get(ctx, common.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil || a == nil {
|
||||
return &dto.AvatarGetRes{}, nil
|
||||
|
||||
@@ -27,7 +27,7 @@ func (c *body_measurement) Save(ctx context.Context, req *dto.BodyMeasurementSav
|
||||
return &struct{}{}, nil
|
||||
}
|
||||
|
||||
func (c *body_measurement) Get(ctx context.Context, req *struct{}) (res *dto.BodyMeasurementGetRes, err error) {
|
||||
func (c *body_measurement) Get(ctx context.Context, req *dto.BodyMeasurementGetReq) (res *dto.BodyMeasurementGetRes, err error) {
|
||||
b, err := service.BodyMeasurementService.Get(ctx, common.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil || b == nil {
|
||||
return &dto.BodyMeasurementGetRes{}, nil
|
||||
|
||||
@@ -11,7 +11,7 @@ type hairstyle struct{}
|
||||
|
||||
var Hairstyle = new(hairstyle)
|
||||
|
||||
func (c *hairstyle) List(ctx context.Context, req *struct{}) (res *dto.HairstyleListRes, err error) {
|
||||
func (c *hairstyle) List(ctx context.Context, req *dto.HairstyleListReq) (res *dto.HairstyleListRes, err error) {
|
||||
list, err := service.HairstyleService.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
commonHttp "slogan-agent/common"
|
||||
"slogan-agent/styleagent/model/dto"
|
||||
"slogan-agent/styleagent/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// member 为 /member/* 簇共享控制器类型(struct 名 kebab-case 决定路由前缀,
|
||||
// 多表拆分文件但保持同一类型,避免路径漂移)
|
||||
type member struct{}
|
||||
|
||||
var Member = new(member)
|
||||
|
||||
// PlanList 会员套餐列表
|
||||
func (c *member) PlanList(ctx context.Context, req *dto.MemberPlanListReq) (res *dto.MemberPlanListRes, err error) {
|
||||
list, err := service.MemberPlanService.PlanList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.MemberPlanListRes{List: list}, nil
|
||||
}
|
||||
|
||||
// Status 我的会员状态
|
||||
func (c *member) Status(ctx context.Context, req *dto.MemberStatusReq) (res *dto.MemberStatusRes, err error) {
|
||||
st, err := service.MemberPlanService.Status(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.MemberStatusRes{
|
||||
IsVip: st.IsVip, ExpireAt: st.ExpireAt, PlanName: st.PlanName, Benefits: st.Benefits,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slogan-agent/common"
|
||||
"slogan-agent/styleagent/model/dto"
|
||||
"slogan-agent/styleagent/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type outfit struct{}
|
||||
|
||||
var Outfit = new(outfit)
|
||||
|
||||
// Generate 生成穿搭方案(异步任务)
|
||||
func (c *outfit) Generate(ctx context.Context, req *dto.OutfitGenerateReq) (res *dto.OutfitGenerateRes, err error) {
|
||||
taskId, err := service.OutfitService.Generate(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.OutfitGenerateRes{TaskId: taskId}, nil
|
||||
}
|
||||
|
||||
// TaskStatus 查询生成任务状态
|
||||
func (c *outfit) TaskStatus(ctx context.Context, req *dto.OutfitTaskStatusReq) (res *dto.OutfitTaskStatusRes, err error) {
|
||||
status, msg, err := service.OutfitService.GetTaskStatus(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.TaskId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.OutfitTaskStatusRes{Status: status, Error: msg}, nil
|
||||
}
|
||||
|
||||
// PlanList 方案列表
|
||||
func (c *outfit) PlanList(ctx context.Context, req *dto.OutfitPlanListReq) (res *dto.OutfitPlanListRes, err error) {
|
||||
list, err := service.OutfitService.ListPlans(ctx, common.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.OutfitPlanListRes{List: list}, nil
|
||||
}
|
||||
|
||||
// PlanDetail 方案详情(items + images + hairstyle)
|
||||
func (c *outfit) PlanDetail(ctx context.Context, req *dto.OutfitPlanDetailReq) (res *dto.OutfitPlanDetailRes, err error) {
|
||||
return service.OutfitService.GetPlanDetail(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.PlanId)
|
||||
}
|
||||
|
||||
// SelectMain 选定主方案(触发效果图生成)
|
||||
func (c *outfit) SelectMain(ctx context.Context, req *dto.OutfitSelectMainReq) (res *struct{}, err error) {
|
||||
if err = service.OutfitService.SelectMain(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.PlanId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &struct{}{}, nil
|
||||
}
|
||||
|
||||
// Review 方案反馈
|
||||
func (c *outfit) Review(ctx context.Context, req *dto.OutfitReviewReq) (res *struct{}, err error) {
|
||||
if err = service.OutfitService.Review(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.PlanId, req.Action, req.Note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &struct{}{}, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slogan-agent/common"
|
||||
"slogan-agent/styleagent/model/dto"
|
||||
"slogan-agent/styleagent/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// outfit 为 /outfit/* 簇共享控制器类型(struct 名 kebab-case 决定路由前缀,
|
||||
// 多表拆分文件但保持同一类型,避免路径漂移)
|
||||
type outfit struct{}
|
||||
|
||||
var Outfit = new(outfit)
|
||||
|
||||
// Generate 生成穿搭方案(异步任务)
|
||||
func (c *outfit) Generate(ctx context.Context, req *dto.OutfitGenerateReq) (res *dto.OutfitGenerateRes, err error) {
|
||||
taskId, err := service.OutfitService.Generate(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.OutfitGenerateRes{TaskId: taskId}, nil
|
||||
}
|
||||
|
||||
// TaskStatus 查询生成任务状态
|
||||
func (c *outfit) TaskStatus(ctx context.Context, req *dto.OutfitTaskStatusReq) (res *dto.OutfitTaskStatusRes, err error) {
|
||||
status, msg, err := service.OutfitService.GetTaskStatus(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.TaskId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.OutfitTaskStatusRes{Status: status, Error: msg}, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slogan-agent/common"
|
||||
"slogan-agent/styleagent/model/dto"
|
||||
"slogan-agent/styleagent/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// PlanList 方案列表
|
||||
func (c *outfit) PlanList(ctx context.Context, req *dto.OutfitPlanListReq) (res *dto.OutfitPlanListRes, err error) {
|
||||
list, err := service.OutfitPlanService.ListPlans(ctx, common.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.OutfitPlanListRes{List: list}, nil
|
||||
}
|
||||
|
||||
// PlanDetail 方案详情(items + images + hairstyle)
|
||||
func (c *outfit) PlanDetail(ctx context.Context, req *dto.OutfitPlanDetailReq) (res *dto.OutfitPlanDetailRes, err error) {
|
||||
return service.OutfitPlanService.GetPlanDetail(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.PlanId)
|
||||
}
|
||||
|
||||
// SelectMain 选定主方案(触发效果图生成)
|
||||
func (c *outfit) SelectMain(ctx context.Context, req *dto.OutfitSelectMainReq) (res *struct{}, err error) {
|
||||
if err = service.OutfitPlanService.SelectMain(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.PlanId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &struct{}{}, nil
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package controller
|
||||
|
||||
// pay_notify_log 表控制器:无独立路由 handler(回调日志由 /member/order/notify 审计写入)
|
||||
+1
-25
@@ -13,30 +13,6 @@ import (
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type member struct{}
|
||||
|
||||
var Member = new(member)
|
||||
|
||||
// PlanList 会员套餐列表
|
||||
func (c *member) PlanList(ctx context.Context, req *dto.MemberPlanListReq) (res *dto.MemberPlanListRes, err error) {
|
||||
list, err := service.MemberPlanService.PlanList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.MemberPlanListRes{List: list}, nil
|
||||
}
|
||||
|
||||
// Status 我的会员状态
|
||||
func (c *member) Status(ctx context.Context, req *dto.MemberStatusReq) (res *dto.MemberStatusRes, err error) {
|
||||
st, err := service.MemberPlanService.Status(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.MemberStatusRes{
|
||||
IsVip: st.IsVip, ExpireAt: st.ExpireAt, PlanName: st.PlanName, Benefits: st.Benefits,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OrderCreate 下单 → 返回支付 URL
|
||||
func (c *member) OrderCreate(ctx context.Context, req *dto.MemberOrderCreateReq) (res *dto.MemberOrderCreateRes, err error) {
|
||||
order, payURL, err := service.PaymentOrderService.CreateMemberOrder(ctx, commonHttp.GetUserId(g.RequestFromCtx(ctx)), req.PlanId)
|
||||
@@ -60,7 +36,7 @@ func (c *member) OrderStatus(ctx context.Context, req *dto.MemberOrderStatusReq)
|
||||
}
|
||||
|
||||
// MemberNotify 虎皮棋支付回调:验签 → 幂等开通 → 返回裸文本 "success"
|
||||
// 虎皮棋要求回调响应体为字面 "success",故不走统一 JSON 包装
|
||||
// 虎皮棋要求回调响应体为字面 "success",故不走统一 JSON 包装(main.go 手动绑定)
|
||||
func MemberNotify(r *ghttp.Request) {
|
||||
ctx := r.Context()
|
||||
body := r.GetBodyString()
|
||||
@@ -0,0 +1,4 @@
|
||||
package controller
|
||||
|
||||
// plan_effect_image 表控制器:无独立路由 handler,逻辑归属 outfit/* 簇
|
||||
// (效果图由选定主方案后异步生成,经 /outfit/plan/detail 返回)
|
||||
@@ -0,0 +1,4 @@
|
||||
package controller
|
||||
|
||||
// plan_outfit_item 表控制器:无独立路由 handler,逻辑归属 outfit/* 簇
|
||||
// (方案条目数据由 /outfit/plan/detail 承载)
|
||||
@@ -0,0 +1,19 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slogan-agent/common"
|
||||
"slogan-agent/styleagent/model/dto"
|
||||
"slogan-agent/styleagent/service"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// Review 方案反馈
|
||||
func (c *outfit) Review(ctx context.Context, req *dto.OutfitReviewReq) (res *struct{}, err error) {
|
||||
if err = service.PlanReviewService.Review(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.PlanId, req.Action, req.Note); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &struct{}{}, nil
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package controller
|
||||
|
||||
// scoring_rule 表控制器:无独立路由 handler,规则在服务端读取(评分阈值/效果图额度)
|
||||
@@ -38,7 +38,7 @@ func (c *user) ChangePassword(ctx context.Context, req *dto.ChangePasswordReq) (
|
||||
return nil, service.UserService.ChangePassword(ctx, common.GetUserId(g.RequestFromCtx(ctx)), req.OldPassword, req.NewPassword)
|
||||
}
|
||||
|
||||
func (c *user) Profile(ctx context.Context, req *struct{}) (res *dto.ProfileRes, err error) {
|
||||
func (c *user) Profile(ctx context.Context, req *dto.ProfileReq) (res *dto.ProfileRes, err error) {
|
||||
user, err := dao.User.GetOne(ctx, common.GetUserId(g.RequestFromCtx(ctx)))
|
||||
if err != nil || user == nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package controller
|
||||
|
||||
// user_member 表控制器:无独立路由 handler(会员状态经 /member/status 返回,
|
||||
// 开通由支付回调与广告激励写入)
|
||||
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type AdRewardClaimReq struct {
|
||||
g.Meta `path:"/reward/claim" method:"post" tags:"广告" summary:"领取广告激励"`
|
||||
AdType string `v:"required|in:effect_extra,vip_trial" json:"ad_type"`
|
||||
}
|
||||
|
||||
type AdRewardInfo struct {
|
||||
AdType string `json:"ad_type"`
|
||||
RemainingToday int `json:"remaining_today"`
|
||||
}
|
||||
|
||||
type AdRewardClaimRes struct {
|
||||
Reward *AdRewardInfo `json:"reward"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type AvatarBuildReq struct {
|
||||
g.Meta `path:"/build" method:"post" tags:"化身" summary:"构建化身"`
|
||||
}
|
||||
|
||||
type AvatarBuildRes struct {
|
||||
AvatarId int64 `json:"avatar_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AvatarGetReq struct {
|
||||
g.Meta `path:"/get" method:"get" tags:"化身" summary:"我的化身"`
|
||||
}
|
||||
|
||||
type AvatarGetRes struct {
|
||||
FaceTemplateId int `json:"face_template_id"`
|
||||
BodyTemplateId int `json:"body_template_id"`
|
||||
SkinToneIndex int `json:"skin_tone_index"`
|
||||
GlbUrl string `json:"glb_url"`
|
||||
BuildStatus string `json:"build_status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type BodyMeasurementSaveReq struct {
|
||||
g.Meta `path:"/save" method:"post" tags:"身形" summary:"保存身形参数"`
|
||||
Height int `json:"height"`
|
||||
Weight int `json:"weight"`
|
||||
SkinTone int `v:"in:1,2,3,4,5" json:"skin_tone"`
|
||||
FitParams string `json:"fit_params"`
|
||||
}
|
||||
|
||||
type BodyMeasurementGetReq struct {
|
||||
g.Meta `path:"/get" method:"get" tags:"身形" summary:"我的身形参数"`
|
||||
}
|
||||
|
||||
type BodyMeasurementGetRes struct {
|
||||
Height int `json:"height"`
|
||||
Weight int `json:"weight"`
|
||||
SkinTone int `json:"skin_tone"`
|
||||
FitParams string `json:"fit_params"`
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type LoginReq struct {
|
||||
g.Meta `path:"/login" method:"post" tags:"用户" summary:"登录"`
|
||||
Account string `v:"required" json:"account"`
|
||||
Password string `v:"required" json:"password"`
|
||||
}
|
||||
|
||||
type LoginRes struct {
|
||||
Token string `json:"token"`
|
||||
User *LoginUser `json:"user"`
|
||||
}
|
||||
|
||||
type LoginUser struct {
|
||||
Id int64 `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ChangePasswordReq struct {
|
||||
g.Meta `path:"/change-password" method:"post" tags:"用户" summary:"修改密码"`
|
||||
OldPassword string `v:"required" json:"old_password"`
|
||||
NewPassword string `v:"required|min-length:6" json:"new_password"`
|
||||
}
|
||||
|
||||
type RegisterReq struct {
|
||||
g.Meta `path:"/register" method:"post" tags:"用户" summary:"注册"`
|
||||
Account string `v:"required" json:"account"`
|
||||
Password string `v:"required|min-length:6" json:"password"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ProfileRes struct {
|
||||
Id int64 `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
type UserPhotoUploadReq struct {
|
||||
g.Meta `path:"/upload" method:"post" tags:"照片" summary:"上传照片"`
|
||||
Type int `v:"required|in:1,2,3,4" json:"type"`
|
||||
}
|
||||
|
||||
type UserPhotoUploadRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type UserPhotoListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"照片" summary:"照片列表"`
|
||||
Type int `json:"type"`
|
||||
}
|
||||
|
||||
type UserPhotoListRes struct {
|
||||
List []*entity.UserPhoto `json:"list"`
|
||||
}
|
||||
|
||||
type UserPhotoDeleteReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"照片" summary:"删除照片"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
|
||||
type WardrobeUploadReq struct {
|
||||
g.Meta `path:"/upload" method:"post" tags:"衣橱" summary:"上传服装"`
|
||||
Category string `v:"required|in:上衣,下装,鞋,配饰" json:"category"`
|
||||
Season string `json:"season"`
|
||||
StyleTags string `json:"style_tags"`
|
||||
ColorInfo string `json:"color_info"`
|
||||
}
|
||||
|
||||
type WardrobeUploadRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type WardrobeListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"衣橱" summary:"衣橱列表"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type WardrobeListRes struct {
|
||||
List []*entity.WardrobeItem `json:"list"`
|
||||
}
|
||||
|
||||
type WardrobeUpdateReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"衣橱" summary:"更新服装"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
Category string `json:"category"`
|
||||
Season string `json:"season"`
|
||||
StyleTags string `json:"style_tags"`
|
||||
}
|
||||
|
||||
type WardrobeDeleteReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"衣橱" summary:"删除服装"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
|
||||
type BodyMeasurementSaveReq struct {
|
||||
g.Meta `path:"/save" method:"post" tags:"身形" summary:"保存身形参数"`
|
||||
Height int `json:"height"`
|
||||
Weight int `json:"weight"`
|
||||
SkinTone int `v:"in:1,2,3,4,5" json:"skin_tone"`
|
||||
FitParams string `json:"fit_params"`
|
||||
}
|
||||
|
||||
type BodyMeasurementGetRes struct {
|
||||
Height int `json:"height"`
|
||||
Weight int `json:"weight"`
|
||||
SkinTone int `json:"skin_tone"`
|
||||
FitParams string `json:"fit_params"`
|
||||
}
|
||||
|
||||
type AvatarBuildReq struct {
|
||||
g.Meta `path:"/build" method:"post" tags:"化身" summary:"构建化身"`
|
||||
}
|
||||
|
||||
type AvatarBuildRes struct {
|
||||
AvatarId int64 `json:"avatar_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AvatarGetRes struct {
|
||||
FaceTemplateId int `json:"face_template_id"`
|
||||
BodyTemplateId int `json:"body_template_id"`
|
||||
SkinToneIndex int `json:"skin_tone_index"`
|
||||
GlbUrl string `json:"glb_url"`
|
||||
BuildStatus string `json:"build_status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type HairstyleListRes struct {
|
||||
List []*entity.HairstyleAsset `json:"list"`
|
||||
}
|
||||
|
||||
type OutfitGenerateReq struct {
|
||||
g.Meta `path:"/generate" method:"post" tags:"穿搭" summary:"生成穿搭方案"`
|
||||
StartDate string `v:"required|date" json:"start_date"`
|
||||
EndDate string `v:"required|date" json:"end_date"`
|
||||
Location string `v:"required" json:"location"`
|
||||
}
|
||||
|
||||
type OutfitGenerateRes struct {
|
||||
TaskId int64 `json:"task_id"`
|
||||
}
|
||||
|
||||
type OutfitTaskStatusReq struct {
|
||||
g.Meta `path:"/task/status" method:"get" tags:"穿搭" summary:"任务状态"`
|
||||
TaskId int64 `v:"required" json:"task_id"`
|
||||
}
|
||||
|
||||
type OutfitTaskStatusRes struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type OutfitPlanListReq struct {
|
||||
g.Meta `path:"/plan/list" method:"get" tags:"穿搭" summary:"方案列表"`
|
||||
}
|
||||
|
||||
type OutfitPlanListRes struct {
|
||||
List []*entity.OutfitPlan `json:"list"`
|
||||
}
|
||||
|
||||
type OutfitPlanDetailReq struct {
|
||||
g.Meta `path:"/plan/detail" method:"get" tags:"穿搭" summary:"方案详情"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
|
||||
type OutfitPlanDetailRes struct {
|
||||
Plan *entity.OutfitPlan `json:"plan"`
|
||||
Items []*entity.PlanOutfitItem `json:"items"`
|
||||
Images []*entity.PlanEffectImage `json:"images"`
|
||||
Hairstyle *entity.HairstyleAsset `json:"hairstyle,omitempty"`
|
||||
}
|
||||
|
||||
type OutfitSelectMainReq struct {
|
||||
g.Meta `path:"/plan/select-main" method:"post" tags:"穿搭" summary:"选定主方案"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
|
||||
type OutfitReviewReq struct {
|
||||
g.Meta `path:"/plan/review" method:"post" tags:"穿搭" summary:"方案反馈"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
Action string `v:"required|in:fav,unfav" json:"action"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type StoreListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"门店" summary:"合作门店列表"`
|
||||
Type int `json:"type"`
|
||||
}
|
||||
|
||||
type StoreListRes struct {
|
||||
List []*entity.PartnerStore `json:"list"`
|
||||
}
|
||||
|
||||
type MemberPlanListReq struct {
|
||||
g.Meta `path:"/plan/list" method:"get" tags:"会员" summary:"会员套餐列表"`
|
||||
}
|
||||
|
||||
type MemberPlanListRes struct {
|
||||
List []*entity.MemberPlan `json:"list"`
|
||||
}
|
||||
|
||||
type MemberStatusReq struct {
|
||||
g.Meta `path:"/status" method:"get" tags:"会员" summary:"我的会员状态"`
|
||||
}
|
||||
|
||||
type MemberStatusRes struct {
|
||||
IsVip bool `json:"is_vip"`
|
||||
ExpireAt string `json:"expire_at"`
|
||||
PlanName string `json:"plan_name"`
|
||||
Benefits []string `json:"benefits"`
|
||||
}
|
||||
|
||||
type MemberOrderCreateReq struct {
|
||||
g.Meta `path:"/order/create" method:"post" tags:"会员" summary:"创建支付订单"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
|
||||
type MemberOrderCreateRes struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
PayUrl string `json:"pay_url"`
|
||||
}
|
||||
|
||||
type MemberOrderStatusReq struct {
|
||||
g.Meta `path:"/order/status" method:"get" tags:"会员" summary:"订单状态"`
|
||||
OrderNo string `v:"required" json:"order_no"`
|
||||
}
|
||||
|
||||
type MemberOrderStatusRes struct {
|
||||
Status string `json:"status"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
PaidAt string `json:"paid_at"`
|
||||
}
|
||||
|
||||
type AdRewardClaimReq struct {
|
||||
g.Meta `path:"/reward/claim" method:"post" tags:"广告" summary:"领取广告激励"`
|
||||
AdType string `v:"required|in:effect_extra,vip_trial" json:"ad_type"`
|
||||
}
|
||||
|
||||
type AdRewardInfo struct {
|
||||
AdType string `json:"ad_type"`
|
||||
RemainingToday int `json:"remaining_today"`
|
||||
}
|
||||
|
||||
type AdRewardClaimRes struct {
|
||||
Reward *AdRewardInfo `json:"reward"`
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type HairstyleListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"发型" summary:"发型资产列表"`
|
||||
}
|
||||
|
||||
type HairstyleListRes struct {
|
||||
List []*entity.HairstyleAsset `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type MemberPlanListReq struct {
|
||||
g.Meta `path:"/plan/list" method:"get" tags:"会员" summary:"会员套餐列表"`
|
||||
}
|
||||
|
||||
type MemberPlanListRes struct {
|
||||
List []*entity.MemberPlan `json:"list"`
|
||||
}
|
||||
|
||||
type MemberStatusReq struct {
|
||||
g.Meta `path:"/status" method:"get" tags:"会员" summary:"我的会员状态"`
|
||||
}
|
||||
|
||||
type MemberStatusRes struct {
|
||||
IsVip bool `json:"is_vip"`
|
||||
ExpireAt string `json:"expire_at"`
|
||||
PlanName string `json:"plan_name"`
|
||||
Benefits []string `json:"benefits"`
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type OutfitGenerateReq struct {
|
||||
g.Meta `path:"/generate" method:"post" tags:"穿搭" summary:"生成穿搭方案"`
|
||||
StartDate string `v:"required|date" json:"start_date"`
|
||||
EndDate string `v:"required|date" json:"end_date"`
|
||||
Location string `v:"required" json:"location"`
|
||||
}
|
||||
|
||||
type OutfitGenerateRes struct {
|
||||
TaskId int64 `json:"task_id"`
|
||||
}
|
||||
|
||||
type OutfitTaskStatusReq struct {
|
||||
g.Meta `path:"/task/status" method:"get" tags:"穿搭" summary:"任务状态"`
|
||||
TaskId int64 `v:"required" json:"task_id"`
|
||||
}
|
||||
|
||||
type OutfitTaskStatusRes struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type OutfitPlanListReq struct {
|
||||
g.Meta `path:"/plan/list" method:"get" tags:"穿搭" summary:"方案列表"`
|
||||
}
|
||||
|
||||
type OutfitPlanListRes struct {
|
||||
List []*entity.OutfitPlan `json:"list"`
|
||||
}
|
||||
|
||||
type OutfitPlanDetailReq struct {
|
||||
g.Meta `path:"/plan/detail" method:"get" tags:"穿搭" summary:"方案详情"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
|
||||
type OutfitPlanDetailRes struct {
|
||||
Plan *entity.OutfitPlan `json:"plan"`
|
||||
Items []*entity.PlanOutfitItem `json:"items"`
|
||||
Images []*entity.PlanEffectImage `json:"images"`
|
||||
Hairstyle *entity.HairstyleAsset `json:"hairstyle,omitempty"`
|
||||
}
|
||||
|
||||
type OutfitSelectMainReq struct {
|
||||
g.Meta `path:"/plan/select-main" method:"post" tags:"穿搭" summary:"选定主方案"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type StoreListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"门店" summary:"合作门店列表"`
|
||||
Type int `json:"type"`
|
||||
}
|
||||
|
||||
type StoreListRes struct {
|
||||
List []*entity.PartnerStore `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package dto
|
||||
|
||||
// 支付回调日志(pay_notify_log)为服务端记录,无 HTTP 接口。
|
||||
@@ -0,0 +1,26 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type MemberOrderCreateReq struct {
|
||||
g.Meta `path:"/order/create" method:"post" tags:"会员" summary:"创建支付订单"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
}
|
||||
|
||||
type MemberOrderCreateRes struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
PayUrl string `json:"pay_url"`
|
||||
}
|
||||
|
||||
type MemberOrderStatusReq struct {
|
||||
g.Meta `path:"/order/status" method:"get" tags:"会员" summary:"订单状态"`
|
||||
OrderNo string `v:"required" json:"order_no"`
|
||||
}
|
||||
|
||||
type MemberOrderStatusRes struct {
|
||||
Status string `json:"status"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
PaidAt string `json:"paid_at"`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package dto
|
||||
|
||||
// 方案效果图(plan_effect_image)无独立 HTTP 接口,由方案选定流程在服务端生成,客户端经方案详情获取。
|
||||
@@ -0,0 +1,3 @@
|
||||
package dto
|
||||
|
||||
// 方案穿搭项(plan_outfit_item)无独立 HTTP 接口,请求/响应经 outfit_plan_dto.go 透传。
|
||||
@@ -0,0 +1,12 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type OutfitReviewReq struct {
|
||||
g.Meta `path:"/plan/review" method:"post" tags:"穿搭" summary:"方案反馈"`
|
||||
PlanId int64 `v:"required" json:"plan_id"`
|
||||
Action string `v:"required|in:fav,unfav" json:"action"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package dto
|
||||
|
||||
// 评分规则(scoring_rule)为服务端内部配置,无 HTTP 接口。
|
||||
@@ -0,0 +1,47 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type LoginReq struct {
|
||||
g.Meta `path:"/login" method:"post" tags:"用户" summary:"登录"`
|
||||
Account string `v:"required" json:"account"`
|
||||
Password string `v:"required" json:"password"`
|
||||
}
|
||||
|
||||
type LoginRes struct {
|
||||
Token string `json:"token"`
|
||||
User *LoginUser `json:"user"`
|
||||
}
|
||||
|
||||
type LoginUser struct {
|
||||
Id int64 `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ChangePasswordReq struct {
|
||||
g.Meta `path:"/change-password" method:"post" tags:"用户" summary:"修改密码"`
|
||||
OldPassword string `v:"required" json:"old_password"`
|
||||
NewPassword string `v:"required|min-length:6" json:"new_password"`
|
||||
}
|
||||
|
||||
type RegisterReq struct {
|
||||
g.Meta `path:"/register" method:"post" tags:"用户" summary:"注册"`
|
||||
Account string `v:"required" json:"account"`
|
||||
Password string `v:"required|min-length:6" json:"password"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ProfileReq struct {
|
||||
g.Meta `path:"/profile" method:"get" tags:"用户" summary:"我的资料"`
|
||||
}
|
||||
|
||||
type ProfileRes struct {
|
||||
Id int64 `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package dto
|
||||
|
||||
// 用户会员关系(user_member)由支付回调/下单流程维护,状态经 member_plan_dto.go 返回。
|
||||
@@ -0,0 +1,30 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type UserPhotoUploadReq struct {
|
||||
g.Meta `path:"/upload" method:"post" tags:"照片" summary:"上传照片"`
|
||||
Type int `v:"required|in:1,2,3,4" json:"type"`
|
||||
}
|
||||
|
||||
type UserPhotoUploadRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type UserPhotoListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"照片" summary:"照片列表"`
|
||||
Type int `json:"type"`
|
||||
}
|
||||
|
||||
type UserPhotoListRes struct {
|
||||
List []*entity.UserPhoto `json:"list"`
|
||||
}
|
||||
|
||||
type UserPhotoDeleteReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"照片" summary:"删除照片"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type WardrobeUploadReq struct {
|
||||
g.Meta `path:"/upload" method:"post" tags:"衣橱" summary:"上传服装"`
|
||||
Category string `v:"required|in:上衣,下装,鞋,配饰" json:"category"`
|
||||
Season string `json:"season"`
|
||||
StyleTags string `json:"style_tags"`
|
||||
ColorInfo string `json:"color_info"`
|
||||
}
|
||||
|
||||
type WardrobeUploadRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type WardrobeListReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"衣橱" summary:"衣橱列表"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type WardrobeListRes struct {
|
||||
List []*entity.WardrobeItem `json:"list"`
|
||||
}
|
||||
|
||||
type WardrobeUpdateReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"衣橱" summary:"更新服装"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
Category string `json:"category"`
|
||||
Season string `json:"season"`
|
||||
StyleTags string `json:"style_tags"`
|
||||
}
|
||||
|
||||
type WardrobeDeleteReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"衣橱" summary:"删除服装"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"slogan-agent/styleagent/agent"
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
)
|
||||
|
||||
// candidateSet 一套预筛组合
|
||||
type candidateSet struct {
|
||||
Items []*entity.WardrobeItem
|
||||
HasOuterwear bool
|
||||
}
|
||||
|
||||
// combineCandidates 预筛组合算法:
|
||||
// 按 category 分组 → 按季节过滤 → 确定性轮询组合,最多 3 套互不相同(含外套标记)
|
||||
func combineCandidates(items []*entity.WardrobeItem, season string, maxSets int) []candidateSet {
|
||||
groups := map[string][]*entity.WardrobeItem{}
|
||||
for _, it := range items {
|
||||
if season != "" && it.Season != "" && it.Season != "四季" && it.Season != season {
|
||||
continue
|
||||
}
|
||||
groups[it.Category] = append(groups[it.Category], it)
|
||||
}
|
||||
if len(groups) == 0 || maxSets <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sets []candidateSet
|
||||
cats := []string{"上衣", "下装", "鞋", "配饰"}
|
||||
for i := 0; i < maxSets; i++ {
|
||||
set := candidateSet{}
|
||||
hasOuterwear := false
|
||||
for _, cat := range cats {
|
||||
g := groups[cat]
|
||||
if len(g) == 0 {
|
||||
continue
|
||||
}
|
||||
it := g[i%len(g)]
|
||||
set.Items = append(set.Items, it)
|
||||
if isOuterwear(it) {
|
||||
hasOuterwear = true
|
||||
}
|
||||
}
|
||||
if len(set.Items) == 0 {
|
||||
break
|
||||
}
|
||||
set.HasOuterwear = hasOuterwear
|
||||
sets = append(sets, set)
|
||||
}
|
||||
return sets
|
||||
}
|
||||
|
||||
func isOuterwear(it *entity.WardrobeItem) bool {
|
||||
return it.Category == "上衣" && (it.StyleTags == "" || it.StyleTags == "外套")
|
||||
}
|
||||
|
||||
// toScoringOutfit 转评分用候选
|
||||
func toScoringOutfit(set candidateSet) agent.CandidateOutfit {
|
||||
o := agent.CandidateOutfit{HasOuterwear: set.HasOuterwear}
|
||||
for _, it := range set.Items {
|
||||
o.Items = append(o.Items, agent.WardrobeItem{
|
||||
Category: it.Category,
|
||||
Season: it.Season,
|
||||
ColorInfo: it.ColorInfo,
|
||||
StyleTags: it.StyleTags,
|
||||
})
|
||||
}
|
||||
return o
|
||||
}
|
||||
+81
-97
@@ -128,7 +128,7 @@ func runGenerateTask(ctx context.Context, taskId, userId int64) {
|
||||
|
||||
// 5. 规则评分
|
||||
setTask(consts.TaskStatusScoring, "")
|
||||
threshold := scoringThreshold(ctx)
|
||||
threshold := ScoringRuleService.Threshold(ctx)
|
||||
plans := out.Plans
|
||||
ctxScore := agent.ScoreContext{
|
||||
TempAvg: weatherResult.AvgTemp, Season: weatherResult.Season,
|
||||
@@ -211,7 +211,7 @@ func scorePlan(p agent.PlanCandidate, items []*entity.WardrobeItem, ctxScore age
|
||||
return agent.Score(&out, &ctxScore)
|
||||
}
|
||||
|
||||
// ==================== 查询/操作 ====================
|
||||
// ==================== 查询 ====================
|
||||
|
||||
func (s *outfitService) GetTaskStatus(ctx context.Context, userId, taskId int64) (string, string, error) {
|
||||
t, err := dao.OutfitGenTask.GetOne(ctx, taskId, userId)
|
||||
@@ -221,94 +221,101 @@ func (s *outfitService) GetTaskStatus(ctx context.Context, userId, taskId int64)
|
||||
return t.Status, t.Error, nil
|
||||
}
|
||||
|
||||
func (s *outfitService) ListPlans(ctx context.Context, userId int64) ([]*entity.OutfitPlan, error) {
|
||||
return dao.OutfitPlan.ListByUser(ctx, userId)
|
||||
}
|
||||
// ==================== 天气(原 weather_service 内联) ====================
|
||||
|
||||
func (s *outfitService) GetPlanDetail(ctx context.Context, userId, planId int64) (*dto.OutfitPlanDetailRes, error) {
|
||||
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
|
||||
if err != nil || plan == nil {
|
||||
return nil, errors.New("方案不存在")
|
||||
}
|
||||
res := &dto.OutfitPlanDetailRes{Plan: plan}
|
||||
res.Items, err = dao.PlanOutfitItem.ListByPlan(ctx, planId)
|
||||
var weatherCache = agent.NewTTLCache(6 * time.Hour)
|
||||
|
||||
// GetWeather 地点 + 日期范围 → 天气结果(高德地理编码 + 和风 7 天预报,缓存 6 小时)
|
||||
func GetWeather(ctx context.Context, location, startDate, endDate string) (*agent.WeatherResult, error) {
|
||||
cityCode, err := agent.GetCityCode(ctx, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Images, err = dao.PlanEffectImage.ListByPlan(ctx, planId)
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", cityCode, startDate, endDate)
|
||||
if v, ok := weatherCache.Get(cacheKey); ok {
|
||||
if result, ok := v.(*agent.WeatherResult); ok {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
result, err := agent.GetDaily(ctx, cityCode, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plan.HairstyleId > 0 {
|
||||
res.Hairstyle, _ = dao.HairstyleAsset.GetOne(ctx, plan.HairstyleId)
|
||||
}
|
||||
return res, nil
|
||||
weatherCache.Set(cacheKey, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SelectMain 选定主方案(同任务其他方案清零)+ 异步生成效果图
|
||||
func (s *outfitService) SelectMain(ctx context.Context, userId, planId int64) error {
|
||||
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
|
||||
if err != nil || plan == nil {
|
||||
return errors.New("方案不存在")
|
||||
}
|
||||
if err := dao.OutfitPlan.ClearMainFlag(ctx, plan.TaskId); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dao.OutfitPlan.SetMainFlag(ctx, planId); err != nil {
|
||||
return err
|
||||
}
|
||||
// 异步生成 3 视角效果图
|
||||
EffectImageService.GenerateForPlan(gctx.New(), planId, userId)
|
||||
return nil
|
||||
func weatherSummaryText(w *agent.WeatherResult) string {
|
||||
return fmt.Sprintf("%s(%s),平均 %d℃,%d 天", w.CityCode, w.Season, w.AvgTemp, len(w.Days))
|
||||
}
|
||||
|
||||
func (s *outfitService) Review(ctx context.Context, userId, planId int64, action, note string) error {
|
||||
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
|
||||
if err != nil || plan == nil {
|
||||
return errors.New("方案不存在")
|
||||
// ==================== 预筛组合(原 outfit_combiner 内联) ====================
|
||||
|
||||
// candidateSet 一套预筛组合
|
||||
type candidateSet struct {
|
||||
Items []*entity.WardrobeItem
|
||||
HasOuterwear bool
|
||||
}
|
||||
|
||||
// combineCandidates 预筛组合算法:
|
||||
// 按 category 分组 → 按季节过滤 → 确定性轮询组合,最多 3 套互不相同(含外套标记)
|
||||
func combineCandidates(items []*entity.WardrobeItem, season string, maxSets int) []candidateSet {
|
||||
groups := map[string][]*entity.WardrobeItem{}
|
||||
for _, it := range items {
|
||||
if season != "" && it.Season != "" && it.Season != "四季" && it.Season != season {
|
||||
continue
|
||||
}
|
||||
groups[it.Category] = append(groups[it.Category], it)
|
||||
}
|
||||
_, err = dao.PlanReview.Insert(ctx, &entity.PlanReview{PlanId: planId, UserId: userId, Action: action, Note: note})
|
||||
return err
|
||||
if len(groups) == 0 || maxSets <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sets []candidateSet
|
||||
cats := []string{"上衣", "下装", "鞋", "配饰"}
|
||||
for i := 0; i < maxSets; i++ {
|
||||
set := candidateSet{}
|
||||
hasOuterwear := false
|
||||
for _, cat := range cats {
|
||||
g := groups[cat]
|
||||
if len(g) == 0 {
|
||||
continue
|
||||
}
|
||||
it := g[i%len(g)]
|
||||
set.Items = append(set.Items, it)
|
||||
if isOuterwear(it) {
|
||||
hasOuterwear = true
|
||||
}
|
||||
}
|
||||
if len(set.Items) == 0 {
|
||||
break
|
||||
}
|
||||
set.HasOuterwear = hasOuterwear
|
||||
sets = append(sets, set)
|
||||
}
|
||||
return sets
|
||||
}
|
||||
|
||||
func isOuterwear(it *entity.WardrobeItem) bool {
|
||||
return it.Category == "上衣" && (it.StyleTags == "" || it.StyleTags == "外套")
|
||||
}
|
||||
|
||||
// toScoringOutfit 转评分用候选
|
||||
func toScoringOutfit(set candidateSet) agent.CandidateOutfit {
|
||||
o := agent.CandidateOutfit{HasOuterwear: set.HasOuterwear}
|
||||
for _, it := range set.Items {
|
||||
o.Items = append(o.Items, agent.WardrobeItem{
|
||||
Category: it.Category,
|
||||
Season: it.Season,
|
||||
ColorInfo: it.ColorInfo,
|
||||
StyleTags: it.StyleTags,
|
||||
})
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// ==================== 内部辅助 ====================
|
||||
|
||||
func planSource(p agent.PlanCandidate) string {
|
||||
for _, it := range p.Items {
|
||||
if it.NewItem || it.ItemId == 0 {
|
||||
return consts.PlanSourceRecommend
|
||||
}
|
||||
}
|
||||
return consts.PlanSourceWardrobe
|
||||
}
|
||||
|
||||
func matchHairstyle(name string, all []*entity.HairstyleAsset) int64 {
|
||||
if name == "" {
|
||||
return 0
|
||||
}
|
||||
for _, h := range all {
|
||||
if h.Name == name {
|
||||
return h.Id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func hairstyleListText(ctx context.Context) string {
|
||||
all, err := dao.HairstyleAsset.ListAll(ctx)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
text := ""
|
||||
for i, h := range all {
|
||||
if i > 0 {
|
||||
text += ";"
|
||||
}
|
||||
text += h.Name + "," + h.StyleTag
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func bodyDescText(ctx context.Context, userId int64) string {
|
||||
bm, err := dao.BodyMeasurement.GetByUser(ctx, userId)
|
||||
if err != nil || bm == nil {
|
||||
@@ -317,10 +324,6 @@ func bodyDescText(ctx context.Context, userId int64) string {
|
||||
return fmt.Sprintf("身高 %dcm,体重 %dkg,肤色 %d 档", bm.Height, bm.Weight, bm.SkinTone)
|
||||
}
|
||||
|
||||
func weatherSummaryText(w *agent.WeatherResult) string {
|
||||
return fmt.Sprintf("%s(%s),平均 %d℃,%d 天", w.CityCode, w.Season, w.AvgTemp, len(w.Days))
|
||||
}
|
||||
|
||||
func weekdayOf(date string) string {
|
||||
d, err := time.Parse("2006-01-02", date)
|
||||
if err != nil {
|
||||
@@ -332,22 +335,3 @@ func weekdayOf(date string) string {
|
||||
}
|
||||
return "workday"
|
||||
}
|
||||
|
||||
func scoringThreshold(ctx context.Context) int {
|
||||
threshold := consts.DefaultScoreThreshold
|
||||
rules, err := dao.ScoringRule.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
return threshold
|
||||
}
|
||||
for _, r := range rules {
|
||||
if r.Dimension == "threshold" {
|
||||
var v struct {
|
||||
Pass int `json:"pass"`
|
||||
}
|
||||
if json.Unmarshal([]byte(r.RulesJson), &v) == nil && v.Pass > 0 {
|
||||
return v.Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
return threshold
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"slogan-agent/styleagent/agent"
|
||||
"slogan-agent/styleagent/consts"
|
||||
"slogan-agent/styleagent/dao"
|
||||
"slogan-agent/styleagent/model/dto"
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
type outfitPlanService struct{}
|
||||
|
||||
var OutfitPlanService = new(outfitPlanService)
|
||||
|
||||
func (s *outfitPlanService) ListPlans(ctx context.Context, userId int64) ([]*entity.OutfitPlan, error) {
|
||||
return dao.OutfitPlan.ListByUser(ctx, userId)
|
||||
}
|
||||
|
||||
func (s *outfitPlanService) GetPlanDetail(ctx context.Context, userId, planId int64) (*dto.OutfitPlanDetailRes, error) {
|
||||
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
|
||||
if err != nil || plan == nil {
|
||||
return nil, errors.New("方案不存在")
|
||||
}
|
||||
res := &dto.OutfitPlanDetailRes{Plan: plan}
|
||||
res.Items, err = PlanOutfitItemService.ListByPlan(ctx, planId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Images, err = dao.PlanEffectImage.ListByPlan(ctx, planId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plan.HairstyleId > 0 {
|
||||
res.Hairstyle, _ = dao.HairstyleAsset.GetOne(ctx, plan.HairstyleId)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SelectMain 选定主方案(同任务其他方案清零)+ 异步生成效果图
|
||||
func (s *outfitPlanService) SelectMain(ctx context.Context, userId, planId int64) error {
|
||||
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
|
||||
if err != nil || plan == nil {
|
||||
return errors.New("方案不存在")
|
||||
}
|
||||
if err := dao.OutfitPlan.ClearMainFlag(ctx, plan.TaskId); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dao.OutfitPlan.SetMainFlag(ctx, planId); err != nil {
|
||||
return err
|
||||
}
|
||||
// 异步生成 3 视角效果图
|
||||
EffectImageService.GenerateForPlan(gctx.New(), planId, userId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func planSource(p agent.PlanCandidate) string {
|
||||
for _, it := range p.Items {
|
||||
if it.NewItem || it.ItemId == 0 {
|
||||
return consts.PlanSourceRecommend
|
||||
}
|
||||
}
|
||||
return consts.PlanSourceWardrobe
|
||||
}
|
||||
|
||||
func matchHairstyle(name string, all []*entity.HairstyleAsset) int64 {
|
||||
if name == "" {
|
||||
return 0
|
||||
}
|
||||
for _, h := range all {
|
||||
if h.Name == name {
|
||||
return h.Id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func hairstyleListText(ctx context.Context) string {
|
||||
all, err := dao.HairstyleAsset.ListAll(ctx)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
text := ""
|
||||
for i, h := range all {
|
||||
if i > 0 {
|
||||
text += ";"
|
||||
}
|
||||
text += h.Name + "," + h.StyleTag
|
||||
}
|
||||
return text
|
||||
}
|
||||
+1
-21
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -35,7 +34,7 @@ func (s *effectImageService) run(ctx context.Context, planId, userId int64) {
|
||||
}
|
||||
// 每日限额:VIP 不限;普通用户 = 基础额度 + 广告激励额外次数
|
||||
if !dao.UserMember.IsVip(ctx, userId) {
|
||||
limit := dailyEffectLimit(ctx)
|
||||
limit := ScoringRuleService.EffectLimit(ctx)
|
||||
if limit > 0 {
|
||||
used, _ := dao.PlanEffectImage.CountByUserToday(ctx, userId)
|
||||
extra, _ := dao.AdRewardLog.CountTodayByType(ctx, userId, consts.AdTypeEffectExtra)
|
||||
@@ -112,22 +111,3 @@ func effectCacheKey(plan *entity.OutfitPlan, angle string) string {
|
||||
sum := md5.Sum([]byte(fmt.Sprintf("%d:%s:%s", plan.Id, plan.Title, angle)))
|
||||
return "plan:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func dailyEffectLimit(ctx context.Context) int {
|
||||
limit := consts.DefaultDailyEffectLimit
|
||||
rules, err := dao.ScoringRule.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
return limit
|
||||
}
|
||||
for _, r := range rules {
|
||||
if r.Dimension == "effect_limit" {
|
||||
var v struct {
|
||||
Daily int `json:"daily"`
|
||||
}
|
||||
if json.Unmarshal([]byte(r.RulesJson), &v) == nil && v.Daily > 0 {
|
||||
return v.Daily
|
||||
}
|
||||
}
|
||||
}
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slogan-agent/styleagent/dao"
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
)
|
||||
|
||||
type planOutfitItemService struct{}
|
||||
|
||||
var PlanOutfitItemService = new(planOutfitItemService)
|
||||
|
||||
func (s *planOutfitItemService) ListByPlan(ctx context.Context, planId int64) ([]*entity.PlanOutfitItem, error) {
|
||||
return dao.PlanOutfitItem.ListByPlan(ctx, planId)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"slogan-agent/styleagent/dao"
|
||||
"slogan-agent/styleagent/model/entity"
|
||||
)
|
||||
|
||||
type planReviewService struct{}
|
||||
|
||||
var PlanReviewService = new(planReviewService)
|
||||
|
||||
func (s *planReviewService) Review(ctx context.Context, userId, planId int64, action, note string) error {
|
||||
plan, err := dao.OutfitPlan.GetOne(ctx, planId, userId)
|
||||
if err != nil || plan == nil {
|
||||
return errors.New("方案不存在")
|
||||
}
|
||||
_, err = dao.PlanReview.Insert(ctx, &entity.PlanReview{PlanId: planId, UserId: userId, Action: action, Note: note})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"slogan-agent/styleagent/consts"
|
||||
"slogan-agent/styleagent/dao"
|
||||
)
|
||||
|
||||
type scoringRuleService struct{}
|
||||
|
||||
var ScoringRuleService = new(scoringRuleService)
|
||||
|
||||
// Threshold 方案及格分(scoring_rule.dimension=threshold)
|
||||
func (s *scoringRuleService) Threshold(ctx context.Context) int {
|
||||
threshold := consts.DefaultScoreThreshold
|
||||
rules, err := dao.ScoringRule.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
return threshold
|
||||
}
|
||||
for _, r := range rules {
|
||||
if r.Dimension == "threshold" {
|
||||
var v struct {
|
||||
Pass int `json:"pass"`
|
||||
}
|
||||
if json.Unmarshal([]byte(r.RulesJson), &v) == nil && v.Pass > 0 {
|
||||
return v.Pass
|
||||
}
|
||||
}
|
||||
}
|
||||
return threshold
|
||||
}
|
||||
|
||||
// EffectLimit 每日效果图基础额度(scoring_rule.dimension=effect_limit)
|
||||
func (s *scoringRuleService) EffectLimit(ctx context.Context) int {
|
||||
limit := consts.DefaultDailyEffectLimit
|
||||
rules, err := dao.ScoringRule.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
return limit
|
||||
}
|
||||
for _, r := range rules {
|
||||
if r.Dimension == "effect_limit" {
|
||||
var v struct {
|
||||
Daily int `json:"daily"`
|
||||
}
|
||||
if json.Unmarshal([]byte(r.RulesJson), &v) == nil && v.Daily > 0 {
|
||||
return v.Daily
|
||||
}
|
||||
}
|
||||
}
|
||||
return limit
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"slogan-agent/styleagent/agent"
|
||||
)
|
||||
|
||||
var weatherCache = agent.NewTTLCache(6 * time.Hour)
|
||||
|
||||
// GetWeather 地点 + 日期范围 → 天气结果(高德地理编码 + 和风 7 天预报,缓存 6 小时)
|
||||
func GetWeather(ctx context.Context, location, startDate, endDate string) (*agent.WeatherResult, error) {
|
||||
cityCode, err := agent.GetCityCode(ctx, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", cityCode, startDate, endDate)
|
||||
if v, ok := weatherCache.Get(cacheKey); ok {
|
||||
if result, ok := v.(*agent.WeatherResult); ok {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
result, err := agent.GetDaily(ctx, cityCode, startDate, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
weatherCache.Set(cacheKey, result)
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user