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:
2026-07-31 15:15:04 +08:00
parent 527f2e0d18
commit 775995f3e8
47 changed files with 771 additions and 565 deletions
+1 -1
View File
@@ -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 审计写入)
@@ -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,规则在服务端读取(评分阈值/效果图额度)
+1 -1
View File
@@ -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 返回,
// 开通由支付回调与广告激励写入)
+19
View File
@@ -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"`
}
+27
View File
@@ -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"`
}
-255
View File
@@ -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"`
}
+26
View File
@@ -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"`
}
+32
View File
@@ -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"`
}
+16
View File
@@ -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 接口。
+26
View File
@@ -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 透传。
+12
View File
@@ -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"`
}
+3
View File
@@ -0,0 +1,3 @@
package dto
// 评分规则(scoring_rule)为服务端内部配置,无 HTTP 接口。
+47
View File
@@ -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"`
}
+3
View File
@@ -0,0 +1,3 @@
package dto
// 用户会员关系(user_member)由支付回调/下单流程维护,状态经 member_plan_dto.go 返回。
+30
View File
@@ -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"`
}
+41
View File
@@ -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"`
}
-69
View File
@@ -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
}
@@ -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
}
+95
View File
@@ -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
}
@@ -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)
}
+22
View File
@@ -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
}
-31
View File
@@ -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
}