bugfix
This commit is contained in:
@@ -57,7 +57,7 @@ zt-closet-backend/
|
||||
│ ├── lzcwyjwt/ # JWT 生成/解析
|
||||
│ ├── matting/ # 抠图服务客户端
|
||||
│ ├── migration/ # 数据库迁移:embed 迁移脚本 + Up/Down/Steps/Version/Force
|
||||
│ │ # (目录下直接存放 000001~000016 版本化 SQL,up 建表/down 删表)
|
||||
│ │ # (目录下只存放单一 000001_init,up 建全量表/down 删全量表)
|
||||
│ ├── setting/ # viper 配置读取与配置结构体
|
||||
│ ├── tokenhub/ # 腾讯 Tokenhub 大模型网关客户端(运势/AI 推荐生成 + 调度)
|
||||
│ ├── tracing/ # OpenTelemetry 初始化与日志桥接
|
||||
@@ -130,7 +130,11 @@ go run ./cmd/migrate down # 全量回滚(按依赖倒序删表,慎用
|
||||
go run ./cmd/migrate force 3 # 迁移中断导致 dirty 时,强制修正版本号
|
||||
```
|
||||
|
||||
迁移脚本位于 `pkg/migration`,共 17 个版本、22 张表,以 `go:embed` 打包进二进制;迁移记录保存在 `schema_migrations` 表。字段与索引以 `internal/mysqlmodel/` 的 GORM 模型为准,ER 图见 `design/zt-closet-er.drawio`。
|
||||
迁移脚本位于 `pkg/migration`,**单一版本**(`000001_init`)包含全部 **27 张业务表**,以 `go:embed` 打包进二进制;迁移记录保存在 `schema_migrations` 表。
|
||||
|
||||
> 维护约定:项目当前**无线上数据**,因此不使用增量迁移(ALTER/UPDATE)。结构变更直接修改 `000001_init.up.sql` 里对应表的 `CREATE TABLE` 语句,并同步 `000001_init.down.sql`。
|
||||
|
||||
字段与索引以 `internal/mysqlmodel/` 的 GORM 模型为准,ER 图见 `design/zt-closet-er.drawio`。
|
||||
|
||||
### 4. 启动 AI 推理服务(可选,推荐)
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
9. **收到的穿搭分享在取消或转私后仍泄露内容**
|
||||
- 接收列表联查分享和穿搭,仅返回分享正常、穿搭公开、审核通过且状态正常的记录。
|
||||
- ⚠️ 后续策略变更(见 `接口文档.md` §5.8):「分享给好友」与「公开到社区」已解耦,仅自己可见的穿搭同样可分享;接收列表与分享详情改为**只排除已删除 / 已封禁的穿搭**,不再按 `is_public` / `audit_status` 过滤。此处"转私后不再下发"的旧结论已作废,请勿据此回退代码。
|
||||
|
||||
10. **穿搭、AI 穿搭和衣柜分享的主记录与明细非事务写入**
|
||||
- 创建、更新重建明细和删除统一使用数据库事务,明细失败时主记录一并回滚。
|
||||
|
||||
@@ -67,6 +67,32 @@ type OutfitListResponse struct {
|
||||
List []OutfitCreateResponse `json:"list"`
|
||||
}
|
||||
|
||||
// OutfitCommunityListRequest 社区穿搭列表请求
|
||||
type OutfitCommunityListRequest struct {
|
||||
PageRequest
|
||||
}
|
||||
|
||||
// OutfitCommunityListResponse 社区穿搭列表响应
|
||||
type OutfitCommunityListResponse struct {
|
||||
PageResponse
|
||||
List []OutfitCommunityItem `json:"list"`
|
||||
}
|
||||
|
||||
// OutfitCommunityItem 社区穿搭列表项(含作者信息与互动计数)
|
||||
type OutfitCommunityItem struct {
|
||||
OutfitUUID string `json:"outfit_uuid"`
|
||||
OutfitName string `json:"outfit_name"`
|
||||
OutfitDesc string `json:"outfit_desc"`
|
||||
Scene string `json:"scene"`
|
||||
Source int8 `json:"source"` // 1=用户创建 2=AI推荐
|
||||
CreatedAt int64 `json:"created_at"` // 秒级时间戳
|
||||
Items []OutfitItemResponse `json:"items"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
OwnerAvatar string `json:"owner_avatar"`
|
||||
LikeCount int64 `json:"like_count"`
|
||||
CollectCount int64 `json:"collect_count"`
|
||||
}
|
||||
|
||||
// OutfitDeleteRequest 删除穿搭
|
||||
type OutfitDeleteRequest struct {
|
||||
OutfitUUID string `json:"outfit_uuid" binding:"required"`
|
||||
|
||||
@@ -55,6 +55,48 @@ func (d *ClothesDao) ClothesUpdateByUUID(ctx context.Context, clothesUUID string
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClothesUpdateByUUIDIfStatusIn 条件更新衣物:仅当当前图片处理状态在 statuses 内时才更新,返回是否更新成功。
|
||||
// 用于「处理成功回填」这类必须与状态守卫绑定的写操作:恢复任务可能已把该记录置为失败并回退额度,
|
||||
// 无条件写回会把状态改回成功,造成「额度已退但衣物成功」的不一致。
|
||||
func (d *ClothesDao) ClothesUpdateByUUIDIfStatusIn(ctx context.Context, clothesUUID string, statuses []int8, updates map[string]any) (bool, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClothesUpdateByUUIDIfStatusIn")
|
||||
defer span.End()
|
||||
|
||||
res := d.db.WithContext(ctx).Model(&mysqlmodel.Clothes{}).
|
||||
Where("clothes_uuid = ? AND image_process_status IN ?", clothesUUID, statuses).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
span.RecordError(res.Error)
|
||||
return false, res.Error
|
||||
}
|
||||
return res.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
// ClothesReferencedCount 统计衣物被「穿搭明细 / 衣柜分享明细」引用的数量(删除前校验,>0 表示不可删除)。
|
||||
// 删除被引用的衣物会留下悬挂引用,公开穿搭与分享会变成空壳(明细里查不到衣物,前端静默跳过)。
|
||||
func (d *ClothesDao) ClothesReferencedCount(ctx context.Context, clothesUUID string) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClothesReferencedCount")
|
||||
defer span.End()
|
||||
|
||||
subQuery := "clothes_id IN (SELECT id FROM clothes WHERE clothes_uuid = ? AND deleted_at = 0)"
|
||||
|
||||
var total int64
|
||||
if err := d.db.WithContext(ctx).Model(&mysqlmodel.OutfitItem{}).
|
||||
Where(subQuery, clothesUUID).Count(&total).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
if total > 0 {
|
||||
return total, nil
|
||||
}
|
||||
if err := d.db.WithContext(ctx).Model(&mysqlmodel.ClosetShareItem{}).
|
||||
Where(subQuery, clothesUUID).Count(&total).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// ClothesGetByUUID 按 UUID 查询衣物
|
||||
func (d *ClothesDao) ClothesGetByUUID(ctx context.Context, clothesUUID string) (*mysqlmodel.Clothes, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClothesGetByUUID")
|
||||
|
||||
@@ -86,13 +86,48 @@ func (d *CollectDao) CollectCountByTarget(ctx context.Context, collectType int8,
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CollectListTargetIDsByUserPage 分页查询用户收藏的目标ID列表(仅 status=1),返回 ids 与总数。
|
||||
// CollectCountByTargets 批量统计多个目标的收藏数(仅 status=1),返回 targetID -> count;空入参短路。
|
||||
func (d *CollectDao) CollectCountByTargets(ctx context.Context, collectType int8, targetIDs []int64) (map[int64]int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.CollectCountByTargets")
|
||||
defer span.End()
|
||||
|
||||
counts := make(map[int64]int64, len(targetIDs))
|
||||
if len(targetIDs) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
TargetID int64 `gorm:"column:target_id"`
|
||||
Cnt int64 `gorm:"column:cnt"`
|
||||
}
|
||||
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Collect{}).
|
||||
Select("target_id, COUNT(*) AS cnt").
|
||||
Where("collect_type = ? AND target_id IN ? AND status = ?", collectType, targetIDs, int8(1)).
|
||||
Group("target_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
counts[r.TargetID] = r.Cnt
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// CollectListTargetIDsByUserPage 分页查询用户收藏的目标ID列表(仅 status=1,且目标穿搭对本人可见),返回 ids 与总数。
|
||||
//
|
||||
// 可见性口径与 service.isOutfitPublicVisible 保持一致:本人收藏自己的穿搭始终可见;
|
||||
// 收藏他人穿搭要求「公开(2) + 审核通过(2) + 状态正常(1)」。
|
||||
// 联表过滤而非服务层过滤,是为了让 total 与实际返回条数口径一致
|
||||
//(此前只按 collects 计数,穿搭转私/删除后会返回 total 虚高、页内条数偏少的空洞页)。
|
||||
func (d *CollectDao) CollectListTargetIDsByUserPage(ctx context.Context, collectType int8, userID int64, page, pageSize int) ([]int64, int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.CollectListTargetIDsByUserPage")
|
||||
defer span.End()
|
||||
|
||||
q := d.db.WithContext(ctx).Model(&mysqlmodel.Collect{}).
|
||||
Where("collect_type = ? AND user_id = ? AND status = ?", collectType, userID, int8(1))
|
||||
Joins("JOIN outfits o ON o.id = collects.target_id AND o.deleted_at = 0").
|
||||
Where("collects.collect_type = ? AND collects.user_id = ? AND collects.status = ?", collectType, userID, int8(1)).
|
||||
Where("o.user_id = ? OR (o.is_public = ? AND o.audit_status = ? AND o.status = ?)", userID, 2, 2, 1)
|
||||
|
||||
var total int64
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
@@ -101,7 +136,8 @@ func (d *CollectDao) CollectListTargetIDsByUserPage(ctx context.Context, collect
|
||||
}
|
||||
|
||||
var ids []int64
|
||||
if err := q.Order("id DESC").Offset((page-1)*pageSize).Limit(pageSize).Pluck("target_id", &ids).Error; err != nil {
|
||||
if err := q.Order("collects.id DESC").Offset((page-1)*pageSize).Limit(pageSize).
|
||||
Pluck("collects.target_id", &ids).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -86,6 +86,34 @@ func (d *LikeDao) LikeCountByTarget(ctx context.Context, likeType int8, targetID
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// LikeCountByTargets 批量统计多个目标的点赞数(仅 status=1),返回 targetID -> count;空入参短路。
|
||||
func (d *LikeDao) LikeCountByTargets(ctx context.Context, likeType int8, targetIDs []int64) (map[int64]int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.LikeCountByTargets")
|
||||
defer span.End()
|
||||
|
||||
counts := make(map[int64]int64, len(targetIDs))
|
||||
if len(targetIDs) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
TargetID int64 `gorm:"column:target_id"`
|
||||
Cnt int64 `gorm:"column:cnt"`
|
||||
}
|
||||
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Like{}).
|
||||
Select("target_id, COUNT(*) AS cnt").
|
||||
Where("like_type = ? AND target_id IN ? AND status = ?", likeType, targetIDs, int8(1)).
|
||||
Group("target_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
counts[r.TargetID] = r.Cnt
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// LikeListUserIDsByTarget 查询点赞用户ID列表(仅 status=1)
|
||||
func (d *LikeDao) LikeListUserIDsByTarget(ctx context.Context, likeType int8, targetID int64) ([]int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.LikeListUserIDsByTarget")
|
||||
|
||||
@@ -165,6 +165,28 @@ func (d *OutfitDao) OutfitListByUserID(ctx context.Context, userID int64, isPubl
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// OutfitListCommunity 分页查询社区穿搭(其他用户「公开 + 审核通过 + 状态正常」的穿搭)。
|
||||
// 状态字面量与 service.isOutfitPublicVisible 保持一致:is_public 2=公开;audit_status 2=审核通过;status 1=正常。
|
||||
func (d *OutfitDao) OutfitListCommunity(ctx context.Context, excludeUserID int64, page, pageSize int) ([]mysqlmodel.Outfit, int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitListCommunity")
|
||||
defer span.End()
|
||||
|
||||
var list []mysqlmodel.Outfit
|
||||
var total int64
|
||||
q := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}).
|
||||
Where("is_public = ? AND audit_status = ? AND status = ?", int8(2), int8(2), int8(1)).
|
||||
Where("user_id != ?", excludeUserID)
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&list).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// OutfitItemDao 穿搭明细数据访问
|
||||
type OutfitItemDao struct {
|
||||
db *gorm.DB
|
||||
|
||||
@@ -119,10 +119,18 @@ func (d *OutfitShareReceiverDao) OutfitShareReceivedList(ctx context.Context, re
|
||||
Joins("JOIN outfit_share_receivers r ON r.share_uuid = outfit_shares.share_uuid").
|
||||
Joins("JOIN outfits o ON o.id = outfit_shares.outfit_id").
|
||||
Where("r.receiver_user_id = ?", receiverUserID).
|
||||
Where("outfit_shares.status = ?", 1).
|
||||
Where("o.is_public = ? AND o.audit_status = ? AND o.status = ? AND o.deleted_at = 0", 2, 2, 1)
|
||||
// 可见性:仅排除「已删除」与「已封禁」的穿搭。
|
||||
// 分享与公开状态解耦(仅自己可见的穿搭同样可分享),故不再按 is_public / audit_status 过滤;
|
||||
// 穿搭删除或封禁后不再下发内容。
|
||||
Where("o.status <> ? AND o.deleted_at = 0", 2)
|
||||
if status != 0 {
|
||||
// 显式筛选分享状态:1=正常 2=已取消。
|
||||
// 修复:此前这里无条件叠加 `outfit_shares.status = 1`,与下面的参数条件形成
|
||||
// `status = 1 AND status = 2`,导致传 status=2 恒返回空列表。
|
||||
q = q.Where("outfit_shares.status = ?", status)
|
||||
} else {
|
||||
// 默认只返回正常分享:已取消的分享不应再出现在接收列表(保留「取消分享后不再下发」语义)。
|
||||
q = q.Where("outfit_shares.status = ?", 1)
|
||||
}
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
|
||||
@@ -22,15 +22,6 @@ end
|
||||
return v
|
||||
`)
|
||||
|
||||
// notificationUnreadDecrScript 未读数 -1,保证结果不低于 0
|
||||
var notificationUnreadDecrScript = redis.NewScript(`
|
||||
local v = redis.call('GET', KEYS[1])
|
||||
if v and tonumber(v) > 0 then
|
||||
return redis.call('DECRBY', KEYS[1], 1)
|
||||
end
|
||||
return 0
|
||||
`)
|
||||
|
||||
// NotificationUnreadIncr 用户未读消息数 +1,返回最新值
|
||||
func NotificationUnreadIncr(ctx context.Context, userID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadIncr")
|
||||
@@ -45,20 +36,6 @@ func NotificationUnreadIncr(ctx context.Context, userID int64) (int64, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// NotificationUnreadDecr 用户未读消息数 -1,返回扣减后的值(不低于 0)
|
||||
func NotificationUnreadDecr(ctx context.Context, userID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadDecr")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyNotificationUnread, userID)
|
||||
val, err := notificationUnreadDecrScript.Run(ctx, global.RedisEngine, []string{key}).Int64()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return 0, err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// NotificationUnreadGet 获取用户未读消息数,不存在时返回 0
|
||||
func NotificationUnreadGet(ctx context.Context, userID int64) (int64, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.NotificationUnreadGet")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package redisdao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redismodel"
|
||||
)
|
||||
|
||||
// VirtualPayUnpaidSet 标记某虚拟支付订单「最近一次查单确认未支付」,TTL 内不再重复查询。
|
||||
func VirtualPayUnpaidSet(ctx context.Context, orderNo string, ttl time.Duration) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.VirtualPayUnpaidSet")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyVirtualPayUnpaid, orderNo)
|
||||
if err := global.RedisEngine.Set(ctx, key, 1, ttl).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VirtualPayUnpaidDel 清除负缓存(订单已支付/已处理后调用,让后续流程不再跳过它)。
|
||||
func VirtualPayUnpaidDel(ctx context.Context, orderNo string) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.VirtualPayUnpaidDel")
|
||||
defer span.End()
|
||||
|
||||
key := fmt.Sprintf(redismodel.KeyVirtualPayUnpaid, orderNo)
|
||||
if err := global.RedisEngine.Del(ctx, key).Err(); err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VirtualPayUnpaidBatchGet 批量判断哪些订单命中负缓存。
|
||||
// 用一次 MGET 代替 N 次 GET,避免逐单往返 Redis 放大单轮耗时。
|
||||
func VirtualPayUnpaidBatchGet(ctx context.Context, orderNos []string) (map[string]bool, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "redisdao.VirtualPayUnpaidBatchGet")
|
||||
defer span.End()
|
||||
|
||||
result := make(map[string]bool, len(orderNos))
|
||||
if len(orderNos) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(orderNos))
|
||||
for _, no := range orderNos {
|
||||
keys = append(keys, fmt.Sprintf(redismodel.KeyVirtualPayUnpaid, no))
|
||||
}
|
||||
vals, err := global.RedisEngine.MGet(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
for i, v := range vals {
|
||||
if i >= len(orderNos) {
|
||||
break
|
||||
}
|
||||
if v != nil {
|
||||
result[orderNos[i]] = true
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package redismodel
|
||||
|
||||
// 虚拟支付相关 Redis key。
|
||||
const (
|
||||
// KeyVirtualPayUnpaid 查单补偿的「已确认未支付」负缓存:order_no。
|
||||
// 命中表示最近一次 query_order 反查结果为未支付,本轮跳过该订单,
|
||||
// 避免大量「下单后不付款」的积压订单长期占满扫描名额,把真正已支付的订单挤出去。
|
||||
KeyVirtualPayUnpaid = RedisPrefix + "xpay:unpaid:%s"
|
||||
)
|
||||
@@ -61,6 +61,27 @@ func (h *OutfitHandler) List(c *gin.Context) {
|
||||
response.ToResponse(out)
|
||||
}
|
||||
|
||||
// Community 分页查询社区穿搭(其他用户公开的穿搭)。
|
||||
func (h *OutfitHandler) Community(c *gin.Context) {
|
||||
response := app.NewResponse(c)
|
||||
userID, ok := middleware.GetUserID(c.Request.Context())
|
||||
if !ok {
|
||||
response.ToErrorResponse(errcode.Unauthorized)
|
||||
return
|
||||
}
|
||||
var request dto.OutfitCommunityListRequest
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
response.ToErrorResponse(errcode.InvalidParams.WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
out, e := h.svc.CommunityList(c.Request.Context(), userID, &request)
|
||||
if e != nil {
|
||||
response.ToErrorResponse(e)
|
||||
return
|
||||
}
|
||||
response.ToResponse(out)
|
||||
}
|
||||
|
||||
// Get 获取穿搭详情(uuid 来自路径参数)。
|
||||
func (h *OutfitHandler) Get(c *gin.Context) {
|
||||
response := app.NewResponse(c)
|
||||
|
||||
@@ -88,6 +88,8 @@ func NewRouter() *gin.Engine {
|
||||
// 穿搭
|
||||
auth.POST("/outfit", outfitHandler.Create)
|
||||
auth.GET("/outfit", outfitHandler.List)
|
||||
// 注意:静态路径 /outfit/community 必须先于 /outfit/:uuid 注册,避免被通配路由抢占
|
||||
auth.GET("/outfit/community", outfitHandler.Community)
|
||||
auth.GET("/outfit/:uuid", outfitHandler.Get)
|
||||
auth.PUT("/outfit/:uuid", outfitHandler.Update)
|
||||
auth.DELETE("/outfit/:uuid", outfitHandler.Delete)
|
||||
|
||||
@@ -88,7 +88,8 @@ func (s *AIRecommendService) Create(ctx context.Context, userID int64, request *
|
||||
// 5. 调用大模型生成推荐。
|
||||
requestTime := nowUnix()
|
||||
client := tokenhub.NewClient()
|
||||
result, gerr := client.GenerateOutfitRecommendation(ctx, zodiac, fortune, clothes)
|
||||
// 用户自定义场景(scene_type=2)透传给模型;为空时由模型自行判断。
|
||||
result, gerr := client.GenerateOutfitRecommendation(ctx, zodiac, fortune, clothes, request.Scene)
|
||||
if gerr != nil {
|
||||
return nil, fail(errcode.AIRecommendFailed, "大模型调用失败")
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ func (s *ClosetShareService) List(ctx context.Context, userID int64, request *dt
|
||||
ClothesCount: countByShare[sh.ShareUUID],
|
||||
ExpireTime: sh.ExpireTime,
|
||||
Status: sh.Status,
|
||||
CreatedAt: sh.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -222,10 +222,24 @@ func (s *ClothesService) processImageAsync(parentCtx context.Context, userID int
|
||||
"fail_reason": "",
|
||||
"original_image_path": "",
|
||||
}
|
||||
if uerr := dao.ClothesUpdateByUUID(ctx, clothesUUID, updates); uerr != nil {
|
||||
// 带状态守卫回填:恢复任务可能已把该记录置为失败并回退了额度,
|
||||
// 此时若无条件写回成功,会出现「额度已退但衣物成功」(白送额度)并残留 COS 对象。
|
||||
ok, uerr := dao.ClothesUpdateByUUIDIfStatusIn(ctx, clothesUUID,
|
||||
[]int8{ImageProcessMatting, ImageProcessClassifying, ImageProcessQueuing}, updates)
|
||||
if uerr != nil {
|
||||
slog.ErrorContext(ctx, "update clothes after classify failed", "clothes_uuid", clothesUUID, "error", uerr)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
// 记录已被置失败或已删除:本次结果作废,清理刚上传的 COS 对象避免孤儿。
|
||||
slog.WarnContext(ctx, "clothes no longer processing, discard result",
|
||||
"clothes_uuid", clothesUUID, "cos_key", cosKey)
|
||||
if derr := upload.FileDeleteCos(ctx, cosKey); derr != nil {
|
||||
slog.ErrorContext(ctx, "delete discarded cos image failed",
|
||||
"clothes_uuid", clothesUUID, "cos_key", cosKey, "error", derr)
|
||||
}
|
||||
return
|
||||
}
|
||||
_ = os.Remove(originalPath)
|
||||
}
|
||||
|
||||
@@ -370,8 +384,8 @@ func (s *ClothesService) assertClothesVisibleViaOutfitShare(ctx context.Context,
|
||||
}
|
||||
return errcode.DatabaseError
|
||||
}
|
||||
// 访客查看要求穿搭仍公开可见(属主访问自己的分享始终可查看)。
|
||||
if share.UserID != userID && !isOutfitPublicVisible(outfit) {
|
||||
// 访客只读查看:分享正常 + 穿搭未被封禁即可(分享与公开状态解耦,属主访问自己的分享始终可查看)。
|
||||
if share.UserID != userID && outfit.Status == OutfitStatusBanned {
|
||||
return errcode.OutfitShareNotFound
|
||||
}
|
||||
// 该衣物必须属于该分享的穿搭明细。
|
||||
@@ -433,6 +447,17 @@ func (s *ClothesService) DeleteByUUID(ctx context.Context, userID int64, request
|
||||
return nil, cerr
|
||||
}
|
||||
|
||||
// 引用校验:已被穿搭明细 / 衣柜分享明细引用的衣物不允许删除,否则会留下悬挂引用,
|
||||
// 公开穿搭与分享会变成「空壳」(明细里查不到衣物,前端静默跳过),
|
||||
// 也与接口文档约定的 50001「衣服已被搭配使用,无法删除」保持一致。
|
||||
refCount, rerr := s.clothesDao.ClothesReferencedCount(ctx, request.ClothesUUID)
|
||||
if rerr != nil {
|
||||
return nil, errcode.DatabaseError
|
||||
}
|
||||
if refCount > 0 {
|
||||
return nil, errcode.ClothesInUse
|
||||
}
|
||||
|
||||
// 事务内锁定衣物行 -> 读最新状态 -> 软删除 -> 条件回退额度 -> 写回退流水,
|
||||
// 数值变更与流水同事务强一致,并避免与异步处理失败回退并发导致漏退/双退。
|
||||
vipExpire := getUserVIPExpire(ctx, userID)
|
||||
|
||||
+24
-10
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
|
||||
@@ -100,6 +101,11 @@ func (s *CollectService) List(ctx context.Context, userID int64, request *dto.Co
|
||||
// 穿搭可能已删除,跳过。
|
||||
continue
|
||||
}
|
||||
// 越权防护(纵深防御):DAO 已按可见性联表过滤,此处再挡一次查询与读取之间
|
||||
// 发生的「转私密 / 被封禁 / 审核驳回」变更,避免下发他人非公开穿搭内容。
|
||||
if outfit.UserID != userID && !isOutfitPublicVisible(outfit) {
|
||||
continue
|
||||
}
|
||||
outfitItems, uerr := s.itemResponses(ctx, outfit.ID)
|
||||
if uerr != nil {
|
||||
continue
|
||||
@@ -133,19 +139,27 @@ func (s *CollectService) GetByTarget(ctx context.Context, userID int64, request
|
||||
}
|
||||
targetID := outfit.ID
|
||||
|
||||
// 是否已收藏:Redis 优先,回落 MySQL。
|
||||
isCollected, err := redisdao.CollectIsMember(ctx, CollectTypeOutfit, targetID, userID)
|
||||
if err != nil {
|
||||
if rec, derr := s.collectDao.CollectGetByTargetAndUser(ctx, CollectTypeOutfit, targetID, userID); derr == nil {
|
||||
isCollected = rec.Status == InteractStatusOn
|
||||
// 是否已收藏:以 MySQL 为权威。
|
||||
// Redis 集合带 TTL,过期后会被后续写入重建为「仅含新成员」,导致 SIsMember/SCard 长期低估,
|
||||
// 因此只在 DB 异常时用 Redis 兜底(降级可用性优先于精确性)。
|
||||
isCollected := false
|
||||
if rec, derr := s.collectDao.CollectGetByTargetAndUser(ctx, CollectTypeOutfit, targetID, userID); derr == nil {
|
||||
isCollected = rec.Status == InteractStatusOn
|
||||
} else if !isNotFound(derr) {
|
||||
slog.WarnContext(ctx, "query collect record failed, fallback to redis",
|
||||
"target_id", targetID, "user_id", userID, "error", derr)
|
||||
if v, rerr := redisdao.CollectIsMember(ctx, CollectTypeOutfit, targetID, userID); rerr == nil {
|
||||
isCollected = v
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏总数:Redis 优先,回落 MySQL。
|
||||
count, err := redisdao.CollectCount(ctx, CollectTypeOutfit, targetID)
|
||||
if err != nil || count == 0 {
|
||||
if dbCnt, derr := s.collectDao.CollectCountByTarget(ctx, CollectTypeOutfit, targetID); derr == nil {
|
||||
count = dbCnt
|
||||
// 收藏总数:以 MySQL 为权威(同上,Redis 仅在 DB 异常时兜底)。
|
||||
count, derr := s.collectDao.CollectCountByTarget(ctx, CollectTypeOutfit, targetID)
|
||||
if derr != nil {
|
||||
slog.WarnContext(ctx, "count collect failed, fallback to redis",
|
||||
"target_id", targetID, "error", derr)
|
||||
if rcnt, rerr := redisdao.CollectCount(ctx, CollectTypeOutfit, targetID); rerr == nil {
|
||||
count = rcnt
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-10
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto"
|
||||
@@ -87,19 +88,27 @@ func (s *LikeService) GetByTargetID(ctx context.Context, userID int64, request *
|
||||
}
|
||||
targetID := outfit.ID
|
||||
|
||||
// 是否已点赞:Redis 优先,回落 MySQL。
|
||||
isLiked, err := redisdao.LikeIsMember(ctx, LikeTypeOutfit, targetID, userID)
|
||||
if err != nil {
|
||||
if rec, derr := s.likeDao.LikeGetByTargetAndUser(ctx, LikeTypeOutfit, targetID, userID); derr == nil {
|
||||
isLiked = rec.Status == InteractStatusOn
|
||||
// 是否已点赞:以 MySQL 为权威。
|
||||
// Redis 集合带 TTL,过期后会被后续写入重建为「仅含新成员」,导致 SIsMember/SCard 长期低估,
|
||||
// 因此只在 DB 异常时用 Redis 兜底(降级可用性优先于精确性)。
|
||||
isLiked := false
|
||||
if rec, derr := s.likeDao.LikeGetByTargetAndUser(ctx, LikeTypeOutfit, targetID, userID); derr == nil {
|
||||
isLiked = rec.Status == InteractStatusOn
|
||||
} else if !isNotFound(derr) {
|
||||
slog.WarnContext(ctx, "query like record failed, fallback to redis",
|
||||
"target_id", targetID, "user_id", userID, "error", derr)
|
||||
if v, rerr := redisdao.LikeIsMember(ctx, LikeTypeOutfit, targetID, userID); rerr == nil {
|
||||
isLiked = v
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞总数:Redis 优先,回落 MySQL。
|
||||
count, err := redisdao.LikeCount(ctx, LikeTypeOutfit, targetID)
|
||||
if err != nil || count == 0 {
|
||||
if dbCnt, derr := s.likeDao.LikeCountByTarget(ctx, LikeTypeOutfit, targetID); derr == nil {
|
||||
count = dbCnt
|
||||
// 点赞总数:以 MySQL 为权威(同上,Redis 仅在 DB 异常时兜底)。
|
||||
count, derr := s.likeDao.LikeCountByTarget(ctx, LikeTypeOutfit, targetID)
|
||||
if derr != nil {
|
||||
slog.WarnContext(ctx, "count like failed, fallback to redis",
|
||||
"target_id", targetID, "error", derr)
|
||||
if rcnt, rerr := redisdao.LikeCount(ctx, LikeTypeOutfit, targetID); rerr == nil {
|
||||
count = rcnt
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,43 +84,57 @@ func (s *NotificationService) Read(ctx context.Context, userID int64, request *d
|
||||
|
||||
// 已读则直接返回当前未读数。
|
||||
if n.IsRead == NotificationRead {
|
||||
cnt, _ := redisdao.NotificationUnreadGet(ctx, userID)
|
||||
cnt, cerr := s.unreadCount(ctx, userID)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||
}
|
||||
|
||||
// 条件标记已读(仅未读 -> 已读),防并发重复扣减未读数。
|
||||
ok, err := s.notificationDao.NotificationUpdateReadByUUIDIfUnread(ctx, request.NotificationUUID)
|
||||
if err != nil {
|
||||
// 条件标记已读(仅未读 -> 已读),防并发重复标记。
|
||||
if _, err := s.notificationDao.NotificationUpdateReadByUUIDIfUnread(ctx, request.NotificationUUID); err != nil {
|
||||
return nil, errcode.DatabaseError
|
||||
}
|
||||
if !ok {
|
||||
// 已被并发标记已读,直接返回当前未读数。
|
||||
cnt, _ := redisdao.NotificationUnreadGet(ctx, userID)
|
||||
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||
// 无论本次是否真的标记成功,都按 DB 重新统计并回填缓存:
|
||||
// Redis 计数在 key 过期后会被 INCR 从 1 重建,直接返回 Redis 的 decr 结果会长期少报。
|
||||
cnt, cerr := s.unreadCount(ctx, userID)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
cnt, _ := redisdao.NotificationUnreadDecr(ctx, userID)
|
||||
|
||||
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||
}
|
||||
|
||||
// Unread 获取当前用户未读数,Redis 优先、回落 MySQL。
|
||||
// Unread 获取当前用户未读数(以 MySQL 为权威)。
|
||||
func (s *NotificationService) Unread(ctx context.Context, userID int64) (*dto.NotificationUnreadResponse, *errcode.Error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "svc.NotificationUnread")
|
||||
defer span.End()
|
||||
|
||||
cnt, err := redisdao.NotificationUnreadGet(ctx, userID)
|
||||
if err == nil && cnt > 0 {
|
||||
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||
cnt, cerr := s.unreadCount(ctx, userID)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
|
||||
}
|
||||
|
||||
// Redis 为 0 或异常,回落 MySQL 统计。
|
||||
dbCnt, derr := s.notificationDao.NotificationCountUnread(ctx, userID)
|
||||
if derr != nil {
|
||||
return nil, errcode.DatabaseError
|
||||
// unreadCount 以 MySQL 为权威统计未读数并回填 Redis 缓存。
|
||||
// 此前「Redis 优先、只有 0 或异常才回源」的策略在 key 过期后会长期少报
|
||||
//(key 过期 → 新通知 INCR 从 1 开始 → Unread 读到正数就不再回源),改为 DB 权威。
|
||||
// DB 异常时才用 Redis 兜底,保证降级可用性。
|
||||
func (s *NotificationService) unreadCount(ctx context.Context, userID int64) (int64, *errcode.Error) {
|
||||
cnt, err := s.notificationDao.NotificationCountUnread(ctx, userID)
|
||||
if err != nil {
|
||||
if rcnt, rerr := redisdao.NotificationUnreadGet(ctx, userID); rerr == nil {
|
||||
slog.WarnContext(ctx, "count unread from db failed, fallback to redis",
|
||||
"user_id", userID, "error", err)
|
||||
return rcnt, nil
|
||||
}
|
||||
return 0, errcode.DatabaseError
|
||||
}
|
||||
// 回填 Redis 未读数缓存,避免 key 过期导致后续标记已读时扣减被吞。
|
||||
_ = redisdao.NotificationUnreadSet(ctx, userID, dbCnt)
|
||||
return &dto.NotificationUnreadResponse{UnreadCount: dbCnt}, nil
|
||||
if serr := redisdao.NotificationUnreadSet(ctx, userID, cnt); serr != nil {
|
||||
slog.WarnContext(ctx, "backfill notification unread cache failed",
|
||||
"user_id", userID, "error", serr)
|
||||
}
|
||||
return cnt, nil
|
||||
}
|
||||
|
||||
// buildNotificationResponse 将通知模型映射为响应。
|
||||
|
||||
@@ -146,12 +146,21 @@ func (s *OrderService) PayNotify(ctx context.Context, request *http.Request) *er
|
||||
return errcode.DatabaseError
|
||||
}
|
||||
if data.Amount.Total != order.PayAmount {
|
||||
// 金额不一致:不流转、不发放权益,人工介入。
|
||||
// 这里回的是成功应答(微信不再重试),所以必须把原始报文落库留痕,
|
||||
// 否则这笔钱在系统里将无任何可追溯记录(callback_raw 是唯一能事后对账的字段)。
|
||||
slog.ErrorContext(ctx, "pay notify: amount mismatch",
|
||||
"out_trade_no", data.OutTradeNo,
|
||||
"notify_amount", data.Amount.Total,
|
||||
"order_amount", order.PayAmount,
|
||||
)
|
||||
return nil // 金额不一致,不流转、不发放权益,人工介入。
|
||||
if uerr := s.orderDao.OrderUpdateByOrderNo(ctx, order.OrderNo, map[string]any{
|
||||
"callback_raw": data.Raw,
|
||||
}); uerr != nil {
|
||||
slog.ErrorContext(ctx, "pay notify: record mismatch callback raw failed",
|
||||
"out_trade_no", data.OutTradeNo, "error", uerr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 条件流转:待支付 -> 已支付,防重复回调。
|
||||
|
||||
@@ -22,6 +22,9 @@ type OutfitService struct {
|
||||
outfitItemDao *mysqldao.OutfitItemDao
|
||||
clothesDao *mysqldao.ClothesDao
|
||||
auditLogDao *mysqldao.OutfitAuditLogDao
|
||||
userDao *mysqldao.UserDao
|
||||
likeDao *mysqldao.LikeDao
|
||||
collectDao *mysqldao.CollectDao
|
||||
}
|
||||
|
||||
// NewOutfitService 创建穿搭服务。
|
||||
@@ -31,6 +34,9 @@ func NewOutfitService() *OutfitService {
|
||||
outfitItemDao: mysqldao.NewOutfitItemDao(),
|
||||
clothesDao: mysqldao.NewClothesDao(),
|
||||
auditLogDao: mysqldao.NewOutfitAuditLogDao(),
|
||||
userDao: mysqldao.NewUserDao(),
|
||||
likeDao: mysqldao.NewLikeDao(),
|
||||
collectDao: mysqldao.NewCollectDao(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +181,30 @@ func (s *OutfitService) List(ctx context.Context, userID int64, request *dto.Out
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CommunityList 分页查询社区穿搭:其他用户「公开 + 审核通过 + 状态正常」的穿搭,
|
||||
// 聚合作者信息与点赞/收藏计数后一次性下发(批量查询,避免 N+1)。
|
||||
func (s *OutfitService) CommunityList(ctx context.Context, userID int64, request *dto.OutfitCommunityListRequest) (*dto.OutfitCommunityListResponse, *errcode.Error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "svc.OutfitCommunityList")
|
||||
defer span.End()
|
||||
|
||||
page, pageSize := normalizePage(request.Page, request.PageSize)
|
||||
|
||||
list, total, err := s.outfitDao.OutfitListCommunity(ctx, userID, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, errcode.DatabaseError
|
||||
}
|
||||
|
||||
items, berr := s.buildCommunityItems(ctx, list)
|
||||
if berr != nil {
|
||||
return nil, errcode.DatabaseError
|
||||
}
|
||||
|
||||
return &dto.OutfitCommunityListResponse{
|
||||
PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize},
|
||||
List: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete 删除穿搭(软删除穿搭 + 清理明细)。
|
||||
func (s *OutfitService) Delete(ctx context.Context, userID int64, request *dto.OutfitDeleteRequest) (*dto.OutfitDeleteResponse, *errcode.Error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "svc.OutfitDelete")
|
||||
@@ -414,6 +444,93 @@ func (s *OutfitService) buildOutfitListResponses(ctx context.Context, list []mys
|
||||
return resps, nil
|
||||
}
|
||||
|
||||
// buildCommunityItems 批量组装社区穿搭列表项(明细/衣物/作者/互动计数均批量查询,避免 N+1)。
|
||||
func (s *OutfitService) buildCommunityItems(ctx context.Context, list []mysqlmodel.Outfit) ([]dto.OutfitCommunityItem, error) {
|
||||
items := make([]dto.OutfitCommunityItem, 0, len(list))
|
||||
if len(list) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
outfitIDs := make([]int64, 0, len(list))
|
||||
ownerIDs := make([]int64, 0, len(list))
|
||||
for _, o := range list {
|
||||
outfitIDs = append(outfitIDs, o.ID)
|
||||
ownerIDs = append(ownerIDs, o.UserID)
|
||||
}
|
||||
|
||||
// 1. 穿搭明细 + 衣物(批量)。
|
||||
allItems, err := s.outfitItemDao.OutfitItemListByOutfitIDs(ctx, outfitIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemsByOutfit := make(map[int64][]int64, len(list))
|
||||
clothesIDSet := make(map[int64]struct{}, len(allItems))
|
||||
for _, it := range allItems {
|
||||
itemsByOutfit[it.OutfitID] = append(itemsByOutfit[it.OutfitID], it.ClothesID)
|
||||
clothesIDSet[it.ClothesID] = struct{}{}
|
||||
}
|
||||
clothesIDs := make([]int64, 0, len(clothesIDSet))
|
||||
for id := range clothesIDSet {
|
||||
clothesIDs = append(clothesIDs, id)
|
||||
}
|
||||
clothesByID := make(map[int64]mysqlmodel.Clothes, len(clothesIDs))
|
||||
if len(clothesIDs) > 0 {
|
||||
clothesList, cerr := s.clothesDao.ClothesGetByIDs(ctx, clothesIDs)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
for i := range clothesList {
|
||||
clothesByID[clothesList[i].ID] = clothesList[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 作者信息(批量;头像 COS key 统一转可访问 URL)。
|
||||
ownerByID := make(map[int64]mysqlmodel.User, len(ownerIDs))
|
||||
if owners, uerr := s.userDao.UserGetByIDs(ctx, ownerIDs); uerr == nil {
|
||||
for i := range owners {
|
||||
ownerByID[owners[i].ID] = owners[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 点赞 / 收藏计数(批量)。
|
||||
likeCounts, lerr := s.likeDao.LikeCountByTargets(ctx, LikeTypeOutfit, outfitIDs)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
collectCounts, cerr := s.collectDao.CollectCountByTargets(ctx, CollectTypeOutfit, outfitIDs)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
|
||||
for i := range list {
|
||||
o := &list[i]
|
||||
oItems := make([]dto.OutfitItemResponse, 0, len(itemsByOutfit[o.ID]))
|
||||
for _, cid := range itemsByOutfit[o.ID] {
|
||||
if c, ok := clothesByID[cid]; ok {
|
||||
oItems = append(oItems, buildOutfitItemResponse(&c))
|
||||
}
|
||||
}
|
||||
|
||||
item := dto.OutfitCommunityItem{
|
||||
OutfitUUID: o.OutfitUUID,
|
||||
OutfitName: o.OutfitName,
|
||||
OutfitDesc: o.OutfitDesc,
|
||||
Scene: o.Scene,
|
||||
Source: o.Source,
|
||||
CreatedAt: o.CreatedAt,
|
||||
Items: oItems,
|
||||
LikeCount: likeCounts[o.ID],
|
||||
CollectCount: collectCounts[o.ID],
|
||||
}
|
||||
if owner, ok := ownerByID[o.UserID]; ok {
|
||||
item.OwnerName = owner.Username
|
||||
item.OwnerAvatar = upload.AvatarURL(owner.Avatar)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// buildOutfitItemResponse 组装穿搭明细(processed_image 转为可访问 URL)。
|
||||
func buildOutfitItemResponse(c *mysqlmodel.Clothes) dto.OutfitItemResponse {
|
||||
return dto.OutfitItemResponse{ClothesUUID: c.ClothesUUID, ProcessedImage: clothesImageURL(c.ProcessedImage)}
|
||||
|
||||
@@ -34,7 +34,9 @@ func NewOutfitShareService() *OutfitShareService {
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建穿搭分享。仅能分享本人且已公开的穿搭;不消耗额度。
|
||||
// Create 创建穿搭分享。仅能分享本人的穿搭;分享与「公开到社区」解耦,
|
||||
// 仅自己可见的穿搭同样可生成分享链接(好友凭 share_uuid 只读查看)。
|
||||
// 额度:私密穿搭分享扣 1 次衣柜分享额度(与衣柜分享共用额度池),已公开穿搭分享不扣。
|
||||
func (s *OutfitShareService) Create(ctx context.Context, userID int64, request *dto.OutfitShareCreateRequest) (*dto.OutfitShareCreateResponse, *errcode.Error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "svc.OutfitShareCreate")
|
||||
defer span.End()
|
||||
@@ -49,9 +51,18 @@ func (s *OutfitShareService) Create(ctx context.Context, userID int64, request *
|
||||
if outfit.UserID != userID {
|
||||
return nil, errcode.OutfitNoPermission
|
||||
}
|
||||
// 分享前提:穿搭已公开可见(否则对方打开后无法查看)。
|
||||
if !isOutfitPublicVisible(outfit) {
|
||||
return nil, errcode.InvalidParams.WithDetails("请先公开该穿搭再分享")
|
||||
// 封禁穿搭不可分享;公开状态 / 审核状态不再作为前置条件(分享与公开到社区解耦)。
|
||||
if outfit.Status == OutfitStatusBanned {
|
||||
return nil, errcode.OutfitNoPermission
|
||||
}
|
||||
|
||||
// 额度:仅「仅自己可见」的穿搭分享扣 1 次衣柜分享额度(与衣柜分享共用额度池);
|
||||
// 已公开穿搭在「公开到社区」时已扣过社区分享额度,分享给好友不再重复扣。
|
||||
charged := outfit.IsPublic == OutfitPrivate
|
||||
if charged {
|
||||
if qerr := consumeDailyQuota(ctx, userID, QuotaTypeClosetShare, 1, "私密穿搭分享消耗"); qerr != nil {
|
||||
return nil, qerr
|
||||
}
|
||||
}
|
||||
|
||||
share := &mysqlmodel.OutfitShare{
|
||||
@@ -61,6 +72,10 @@ func (s *OutfitShareService) Create(ctx context.Context, userID int64, request *
|
||||
Status: ShareStatusNormal,
|
||||
}
|
||||
if err := s.shareDao.OutfitShareCreate(ctx, share); err != nil {
|
||||
// 分享落库失败:回退刚扣的额度,避免用户额度被白扣。
|
||||
if charged {
|
||||
refundDailyQuota(ctx, userID, QuotaTypeClosetShare, 1)
|
||||
}
|
||||
return nil, errcode.DatabaseError
|
||||
}
|
||||
return &dto.OutfitShareCreateResponse{ShareUUID: share.ShareUUID}, nil
|
||||
@@ -92,8 +107,9 @@ func (s *OutfitShareService) Detail(ctx context.Context, userID int64, shareUUID
|
||||
}
|
||||
|
||||
isOwner := share.UserID == userID
|
||||
// 访客视角:穿搭必须仍处于公开可见状态,否则分享失效
|
||||
if !isOwner && !isOutfitPublicVisible(outfit) {
|
||||
// 访客视角:分享正常即可只读查看(分享独立于公开状态,取消公开不影响已发出的分享);
|
||||
// 仅当穿搭已删除(上方判为 90103)或已封禁时视为分享失效。
|
||||
if !isOwner && outfit.Status == OutfitStatusBanned {
|
||||
return nil, errcode.OutfitShareNotFound
|
||||
}
|
||||
|
||||
|
||||
+23
-11
@@ -300,10 +300,16 @@ func consumeCosQuota(ctx context.Context, userID int64, n int64, relatedID int64
|
||||
if delta > remain {
|
||||
delta = remain
|
||||
}
|
||||
if err := tx.Model(&mysqlmodel.QuotaGrant{}).
|
||||
res := tx.Model(&mysqlmodel.QuotaGrant{}).
|
||||
Where("id = ? AND used + ? <= amount", grants[i].ID, delta).
|
||||
UpdateColumn("used", gorm.Expr("used + ?", delta)).Error; err != nil {
|
||||
return err
|
||||
UpdateColumn("used", gorm.Expr("used + ?", delta))
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
// 凭证在行锁下被并发改写(理论上不应发生)。此时若继续累加会少扣额度,
|
||||
// 直接回滚整个事务并报额度不足,宁可让用户重试也不超额放行。
|
||||
return errcode.QuotaInsufficient
|
||||
}
|
||||
remain -= delta
|
||||
}
|
||||
@@ -415,25 +421,31 @@ func refundCosQuotaTx(tx *gorm.DB, userID int64, vipExpire int64, n int64, relat
|
||||
}
|
||||
remain -= delta
|
||||
}
|
||||
if remain > 0 {
|
||||
// 无足够已用额度可回退(数据异常),跳过写流水。
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 写回退流水。
|
||||
// 3. 写回退流水:变更量取「实际回退量」。
|
||||
// 此前 `remain > 0` 时直接 return,导致额度已被部分改回却没有流水(账实不一致);
|
||||
// 同时 ChangeNum 写的是请求量 n,退不满时数字虚高。现统一按实际回退量记账,
|
||||
// 退不满/退不动时在原因里标注并告警(已用额度不足通常意味着重复回退或数据异常,必须留痕)。
|
||||
refunded := n - remain
|
||||
flowReason := reason
|
||||
if refunded < n {
|
||||
flowReason = fmt.Sprintf("%s(实际回退 %d/%d,可用已退额度不足)", reason, refunded, n)
|
||||
slog.WarnContext(context.Background(), "refund cos quota partially applied",
|
||||
"user_id", userID, "want", n, "refunded", refunded, "related_id", relatedID)
|
||||
}
|
||||
after := cosRemainTx(tx, userID, vipExpire, now)
|
||||
flow := &mysqlmodel.QuotaFlowLog{
|
||||
QuotaFlowUUID: genUUID(),
|
||||
UserID: userID,
|
||||
ChangeTarget: QuotaChangeTargetUse,
|
||||
ChangeType: QuotaChangeTypeRefund,
|
||||
ChangeNum: n,
|
||||
OldValue: after - n,
|
||||
ChangeNum: refunded,
|
||||
OldValue: after - refunded,
|
||||
NewValue: after,
|
||||
QuotaType: QuotaTypeCos,
|
||||
RelatedID: relatedID,
|
||||
Reason: reason,
|
||||
Reason: flowReason,
|
||||
}
|
||||
// 回退类流水无业务订单号,用流水 UUID 填充 order_no 避免唯一索引空串冲突。
|
||||
flow.OrderNo = flow.QuotaFlowUUID
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -143,6 +144,7 @@ func (s *UserService) InfoGetBySelf(ctx context.Context, userID int64) (*dto.Use
|
||||
|
||||
if user, err := redisdao.UserInfoGet(ctx, userID); err == nil && user != nil {
|
||||
info := buildUserInfoResponse(user)
|
||||
s.consumeNewUserFlag(userID, &info)
|
||||
return &info, nil
|
||||
} else if err != nil && !isCacheMiss(err) {
|
||||
// 缓存异常不阻断,继续回落 DB。
|
||||
@@ -158,9 +160,22 @@ func (s *UserService) InfoGetBySelf(ctx context.Context, userID int64) (*dto.Use
|
||||
_ = redisdao.UserInfoSet(ctx, user, 0)
|
||||
|
||||
info := buildUserInfoResponse(user)
|
||||
s.consumeNewUserFlag(userID, &info)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// consumeNewUserFlag 消费「新用户」标记。
|
||||
// is_new_user 只在注册时写 true,此前没有任何复位路径,会让客户端「新用户引导」反复触发。
|
||||
// 首次读取用户信息即视为引导已消费:本次响应返回 false,并异步复位 DB(best-effort,失败只告警)。
|
||||
// 只在「获取用户信息」路径调用——WxLogin 的 is_new_user 表示「本次是否新注册」,语义保持不变。
|
||||
func (s *UserService) consumeNewUserFlag(userID int64, info *dto.UserInfoGetBySelfResponse) {
|
||||
if !info.IsNewUser {
|
||||
return
|
||||
}
|
||||
info.IsNewUser = false
|
||||
go s.resetNewUserFlag(userID)
|
||||
}
|
||||
|
||||
// ProfileUpdate 修改当前用户资料。
|
||||
func (s *UserService) ProfileUpdate(ctx context.Context, userID int64, request *dto.UserProfileUpdateBySelfRequest) (*dto.UserProfileUpdateBySelfResponse, *errcode.Error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "svc.UserProfileUpdate")
|
||||
@@ -257,9 +272,13 @@ func (s *UserService) AvatarUpload(ctx context.Context, userID int64, fileHeader
|
||||
return nil, errcode.UploadFileSaveFailed
|
||||
}
|
||||
subDir := fmt.Sprintf("%s/%d/avatar", user.UserUUID, userID)
|
||||
if _, serr := upload.FileSaveTmpBytes(ctx, imageData, fileName, subDir); serr != nil {
|
||||
tmpPath, serr := upload.FileSaveTmpBytes(ctx, imageData, fileName, subDir)
|
||||
if serr != nil {
|
||||
return nil, errcode.UploadFileSaveFailed
|
||||
}
|
||||
// 头像不消耗额度、上传接口也无限流,临时文件必须清理,
|
||||
// 否则单个认证用户循环上传即可把磁盘写满(此前返回值被丢弃,文件从不删除)。
|
||||
defer func() { _ = os.Remove(tmpPath) }()
|
||||
cosKey := fmt.Sprintf("%s/%s", subDir, fileName)
|
||||
if cerr := upload.FileUploadCos(ctx, cosKey); cerr != nil {
|
||||
return nil, errcode.UploadFileSaveFailed
|
||||
@@ -415,3 +434,16 @@ func buildUserInfoResponse(user *mysqlmodel.User) dto.UserInfoGetBySelfResponse
|
||||
VIPExpire: user.VIPExpire,
|
||||
}
|
||||
}
|
||||
|
||||
// resetNewUserFlag 异步复位 is_new_user(best-effort,失败仅告警)。
|
||||
// 使用独立 context,避免请求结束后 ctx 被取消导致复位失败。
|
||||
func (s *UserService) resetNewUserFlag(userID int64) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.userDao.UserUpdateByID(ctx, userID, map[string]any{"is_new_user": false}); err != nil {
|
||||
slog.WarnContext(ctx, "reset is_new_user failed", "user_id", userID, "error", err)
|
||||
return
|
||||
}
|
||||
_ = redisdao.UserInfoDel(ctx, userID)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
@@ -17,8 +18,20 @@ const (
|
||||
// virtualConfirmLookbackSeconds 查单补偿的回看窗口:只处理近 2 小时内的待支付/已过期虚拟订单。
|
||||
// 该窗口覆盖推送重试耗尽的时间(官方最多重试 15 次、间隔 2/4/8/16… 秒),又避免全表扫描。
|
||||
virtualConfirmLookbackSeconds int64 = 2 * 60 * 60
|
||||
// virtualConfirmBatchSize 单轮最多处理的订单数,控制单轮耗时与微信接口调用量。
|
||||
virtualConfirmBatchSize = 50
|
||||
// virtualConfirmBatchSize 单轮最多取回的候选订单数。
|
||||
virtualConfirmBatchSize = 200
|
||||
// virtualConfirmWorkers 单轮并发查询的 worker 数。
|
||||
// 必须并发:串行查询在积压时会撞上 ctx 超时,导致尾部订单被静默跳过(看似处理了全部,实则只查了前一部分)。
|
||||
virtualConfirmWorkers = 5
|
||||
// virtualConfirmUnpaidTTL 「已确认未支付」负缓存的存活时间。
|
||||
// 命中即跳过本轮查询,使积压订单让出扫描名额——这是修复「积压订单饿死已支付订单」的关键;
|
||||
// 同时把单个订单的查询频率从每分钟一次降到每 TTL 一次,控制微信接口调用量。
|
||||
// 代价:用户支付后最坏要等一个 TTL 才被补偿发现,需要更快收敛就把该值调小。
|
||||
virtualConfirmUnpaidTTL = 10 * time.Minute
|
||||
// virtualConfirmLockTTL 单轮任务的分布式锁 TTL,必须大于单轮最大耗时,否则锁提前过期会让多实例并发执行。
|
||||
virtualConfirmLockTTL = 5 * time.Minute
|
||||
// virtualConfirmTimeout 单轮任务的上下文超时,需给「批量查询 + 并发反查」留足余量。
|
||||
virtualConfirmTimeout = 10 * time.Minute
|
||||
)
|
||||
|
||||
// StartVirtualPayConfirmScheduler 启动虚拟支付查单补偿定时任务(官方「道具直购流程」的发货轮询分支)。
|
||||
@@ -45,14 +58,20 @@ func StartVirtualPayConfirmScheduler() {
|
||||
}()
|
||||
}
|
||||
|
||||
// runVirtualPayConfirm 扫描待确认的虚拟支付订单并逐个向微信反查,单实例通过 Redis 分布式锁防重。
|
||||
// runVirtualPayConfirm 扫描待确认的虚拟支付订单并并发向微信反查,单实例通过 Redis 分布式锁防重。
|
||||
//
|
||||
// 扫描策略(避免积压饿死):
|
||||
// 1. 按 id 升序取窗口内候选订单,用「已确认未支付」负缓存批量过滤掉最近查过的订单,
|
||||
// 被过滤的订单不再占名额,后续新订单自然会被覆盖到;
|
||||
// 2. 剩余候选用固定 worker 并发反查,避免串行耗时撞上 ctx 超时;
|
||||
// 3. 候选数打满上限时打 warning,提示可能存在积压。
|
||||
func runVirtualPayConfirm() {
|
||||
// 虚拟支付未开通时直接返回,避免无谓的数据库扫描。
|
||||
if !wechat.IsVirtualPayEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), virtualConfirmTimeout)
|
||||
defer cancel()
|
||||
|
||||
ctx, span := global.Tracer.Start(ctx, "scheduler.VirtualPayConfirm")
|
||||
@@ -60,7 +79,7 @@ func runVirtualPayConfirm() {
|
||||
|
||||
lockKey := redismodel.KeyLockVirtualConfirm
|
||||
lockVal := genUUID()
|
||||
ok, lerr := redisdao.TryLock(ctx, lockKey, lockVal, time.Minute)
|
||||
ok, lerr := redisdao.TryLock(ctx, lockKey, lockVal, virtualConfirmLockTTL)
|
||||
if lerr != nil || !ok {
|
||||
return
|
||||
}
|
||||
@@ -76,17 +95,65 @@ func runVirtualPayConfirm() {
|
||||
if len(orders) == 0 {
|
||||
return
|
||||
}
|
||||
if len(orders) >= virtualConfirmBatchSize {
|
||||
slog.WarnContext(ctx, "virtual confirm backlog detected, candidates reached limit",
|
||||
"candidates", len(orders), "limit", virtualConfirmBatchSize,
|
||||
"lookback_seconds", virtualConfirmLookbackSeconds)
|
||||
}
|
||||
|
||||
// 批量过滤负缓存:一次 MGET,避免逐单往返 Redis。
|
||||
orderNos := make([]string, 0, len(orders))
|
||||
for i := range orders {
|
||||
orderNos = append(orderNos, orders[i].OrderNo)
|
||||
}
|
||||
unpaid, merr := redisdao.VirtualPayUnpaidBatchGet(ctx, orderNos)
|
||||
if merr != nil {
|
||||
// 负缓存不可用不阻断补偿:本轮退化为「逐单都查」(最坏情况等价于旧行为)。
|
||||
slog.WarnContext(ctx, "get virtual pay unpaid cache failed, fallback to query all", "error", merr)
|
||||
unpaid = map[string]bool{}
|
||||
}
|
||||
|
||||
targets := make([]*mysqlmodel.Order, 0, len(orders))
|
||||
for i := range orders {
|
||||
if unpaid[orders[i].OrderNo] {
|
||||
continue // 最近已确认未支付,让出名额给其它订单。
|
||||
}
|
||||
targets = append(targets, &orders[i])
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
s := NewOrderService()
|
||||
confirmed := 0
|
||||
for i := range orders {
|
||||
if s.confirmVirtualOrder(ctx, &orders[i]) {
|
||||
confirmed++
|
||||
}
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
confirmed int
|
||||
)
|
||||
sem := make(chan struct{}, virtualConfirmWorkers)
|
||||
for _, order := range targets {
|
||||
wg.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(o *mysqlmodel.Order) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("virtual confirm worker panic", "order_no", o.OrderNo, "recover", r)
|
||||
}
|
||||
}()
|
||||
if s.confirmVirtualOrder(ctx, o) {
|
||||
mu.Lock()
|
||||
confirmed++
|
||||
mu.Unlock()
|
||||
}
|
||||
}(order)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if confirmed > 0 {
|
||||
slog.InfoContext(ctx, "virtual pay confirm compensated orders",
|
||||
"scanned", len(orders), "confirmed", confirmed)
|
||||
"scanned", len(orders), "queried", len(targets), "confirmed", confirmed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,12 +171,18 @@ func (s *OrderService) confirmVirtualOrder(ctx context.Context, order *mysqlmode
|
||||
|
||||
info, qerr := wechat.QueryOrder(ctx, user.WxOpenID, order.OrderNo, "")
|
||||
if qerr != nil {
|
||||
// 查询失败不等于「未支付」,不写负缓存,下一轮继续重试。
|
||||
slog.ErrorContext(ctx, "virtual confirm: query order failed",
|
||||
"order_no", order.OrderNo, "error", qerr)
|
||||
return false
|
||||
}
|
||||
if !wechat.IsOrderPaidStatus(info.Order.Status) {
|
||||
// 未支付:用户放弃或仍在支付中,保持原状态等待过期。
|
||||
// 写入负缓存让该订单在 TTL 内让出扫描名额,避免大量未支付订单把已支付订单挤出扫描窗口。
|
||||
if serr := redisdao.VirtualPayUnpaidSet(ctx, order.OrderNo, virtualConfirmUnpaidTTL); serr != nil {
|
||||
slog.WarnContext(ctx, "virtual confirm: set unpaid cache failed",
|
||||
"order_no", order.OrderNo, "error", serr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
// 金额必须与下单快照一致,防止订单号错配导致错发。
|
||||
@@ -138,6 +211,11 @@ func (s *OrderService) confirmVirtualOrder(ctx context.Context, order *mysqlmode
|
||||
slog.ErrorContext(ctx, "virtual confirm: notify provide goods failed",
|
||||
"order_no", order.OrderNo, "error", nerr)
|
||||
}
|
||||
// 订单已补发,清掉可能残留的负缓存。
|
||||
if derr := redisdao.VirtualPayUnpaidDel(ctx, order.OrderNo); derr != nil {
|
||||
slog.WarnContext(ctx, "virtual confirm: clear unpaid cache failed",
|
||||
"order_no", order.OrderNo, "error", derr)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,15 @@ func setupSetting() error {
|
||||
if global.JWTSetting == nil || global.JWTSetting.SecretKey == "" {
|
||||
return fmt.Errorf("JWT secret key must not be empty")
|
||||
}
|
||||
// 过期时间必须为正数,这里 fail-fast 不静默兜底:
|
||||
// 配 0 会让签出的 token 立即过期(登录即失败),且 TTL=0 传给 Redis 表示「不设过期」,
|
||||
// 会话 key 会永久驻留、无法自然回收。
|
||||
if global.JWTSetting.AccessTokenExpire <= 0 {
|
||||
return fmt.Errorf("JWT AccessTokenExpire must be > 0, got %d", global.JWTSetting.AccessTokenExpire)
|
||||
}
|
||||
if global.JWTSetting.RefreshTokenExpire <= 0 {
|
||||
return fmt.Errorf("JWT RefreshTokenExpire must be > 0, got %d", global.JWTSetting.RefreshTokenExpire)
|
||||
}
|
||||
|
||||
err = setting.ReadSection("Cos", &global.CosSetting)
|
||||
if err != nil {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
@@ -1,28 +0,0 @@
|
||||
CREATE TABLE `users` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户UUID',
|
||||
`wx_open_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信openid',
|
||||
`username` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||
`avatar` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '头像存储位置',
|
||||
`sex` TINYINT NOT NULL DEFAULT 0 COMMENT '性别 1=男 2=女',
|
||||
`birth_day` BIGINT NOT NULL DEFAULT 0 COMMENT '生日',
|
||||
`phone` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '手机号',
|
||||
`is_new_user` TINYINT NOT NULL DEFAULT 0 COMMENT '是否新用户 0=否 1=是',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=正常 2=临时禁用 3=永久禁用 4=已注销',
|
||||
`ban_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '封禁原因',
|
||||
`ban_start` BIGINT NOT NULL DEFAULT 0 COMMENT '封禁开始时间',
|
||||
`ban_expire` BIGINT NOT NULL DEFAULT 0 COMMENT '封禁到期时间',
|
||||
`vip_type` TINYINT NOT NULL DEFAULT 0 COMMENT 'VIP类型 1=非会员 2=月度会员 3=年度会员 4=体验会员',
|
||||
`vip_start` BIGINT NOT NULL DEFAULT 0 COMMENT 'VIP开始时间',
|
||||
`vip_expire` BIGINT NOT NULL DEFAULT 0 COMMENT 'VIP到期时间',
|
||||
`last_login_time` BIGINT NOT NULL DEFAULT 0 COMMENT '最近登录时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_wx_open_id` (`wx_open_id`),
|
||||
KEY `idx_users_user_uuid` (`user_uuid`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户表';
|
||||
@@ -0,0 +1,53 @@
|
||||
-- zt-closet 数据库全量回滚脚本:删除全部业务表。
|
||||
--
|
||||
-- 注意:迁移中不存在任何外键约束,因此删除顺序无依赖,此处按与 up 相反的书写顺序排列仅为可读性。
|
||||
-- 该脚本会一次性删除全部 25 张业务表(不含 golang-migrate 自建的 schema_migrations),生产环境慎用。
|
||||
|
||||
-- 十一、运势与审核
|
||||
DROP TABLE IF EXISTS `outfit_audit_logs`;
|
||||
DROP TABLE IF EXISTS `closet_audit_logs`;
|
||||
DROP TABLE IF EXISTS `horoscopes`;
|
||||
|
||||
-- 十、互动与通知
|
||||
DROP TABLE IF EXISTS `user_notifications`;
|
||||
DROP TABLE IF EXISTS `likes`;
|
||||
DROP TABLE IF EXISTS `collects`;
|
||||
|
||||
-- 九、资金与调用流水
|
||||
DROP TABLE IF EXISTS `ai_horoscope_flow_logs`;
|
||||
DROP TABLE IF EXISTS `ai_recommend_flow_logs`;
|
||||
DROP TABLE IF EXISTS `vip_flow_logs`;
|
||||
DROP TABLE IF EXISTS `refund_flow_logs`;
|
||||
|
||||
-- 八、权益发放与回收(补偿任务)
|
||||
DROP TABLE IF EXISTS `benefit_revoke_tasks`;
|
||||
DROP TABLE IF EXISTS `benefit_grant_tasks`;
|
||||
|
||||
-- 七、额度
|
||||
DROP TABLE IF EXISTS `quota_flow_logs`;
|
||||
DROP TABLE IF EXISTS `quota_grants`;
|
||||
DROP TABLE IF EXISTS `user_daily_usages`;
|
||||
DROP TABLE IF EXISTS `user_quotas`;
|
||||
|
||||
-- 六、商品与订单
|
||||
DROP TABLE IF EXISTS `orders`;
|
||||
DROP TABLE IF EXISTS `goods`;
|
||||
|
||||
-- 五、穿搭分享
|
||||
DROP TABLE IF EXISTS `outfit_share_receivers`;
|
||||
DROP TABLE IF EXISTS `outfit_shares`;
|
||||
|
||||
-- 四、衣柜分享
|
||||
DROP TABLE IF EXISTS `closet_share_receivers`;
|
||||
DROP TABLE IF EXISTS `closet_share_items`;
|
||||
DROP TABLE IF EXISTS `closet_shares`;
|
||||
|
||||
-- 三、穿搭
|
||||
DROP TABLE IF EXISTS `outfit_items`;
|
||||
DROP TABLE IF EXISTS `outfits`;
|
||||
|
||||
-- 二、衣物
|
||||
DROP TABLE IF EXISTS `clothes`;
|
||||
|
||||
-- 一、用户
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
@@ -0,0 +1,622 @@
|
||||
-- zt-closet 数据库全量初始化脚本(单一迁移版本)。
|
||||
--
|
||||
-- 维护约定:
|
||||
-- 1. 项目当前无线上数据,所有结构变更统一合并进本文件,不再新增增量迁移(ALTER / UPDATE)。
|
||||
-- 2. 新增字段或索引时,直接修改下方对应表的 CREATE TABLE 语句,并同步 000001_init.down.sql。
|
||||
-- 3. 执行前需保证目标库为空(脚本只建表,不建库);迁移由 cmd/migrate 手动触发,服务启动不会自动迁移:
|
||||
-- go run ./cmd/migrate up
|
||||
-- 4. 迁移 DSN 已开启 multiStatements,本文件的多条语句可一次性执行。
|
||||
-- 5. 字段与索引以 internal/mysqlmodel/ 的 GORM 模型为准,ER 图见 design/zt-closet-er.drawio。
|
||||
|
||||
-- ============================================================
|
||||
-- 一、用户
|
||||
-- ============================================================
|
||||
CREATE TABLE `users` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户UUID',
|
||||
`wx_open_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信openid',
|
||||
`username` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||
`avatar` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '头像存储位置',
|
||||
`sex` TINYINT NOT NULL DEFAULT 0 COMMENT '性别 1=男 2=女',
|
||||
`birth_day` BIGINT NOT NULL DEFAULT 0 COMMENT '生日',
|
||||
`phone` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '手机号',
|
||||
`is_new_user` TINYINT NOT NULL DEFAULT 0 COMMENT '是否新用户 0=否 1=是',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=正常 2=临时禁用 3=永久禁用 4=已注销',
|
||||
`ban_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '封禁原因',
|
||||
`ban_start` BIGINT NOT NULL DEFAULT 0 COMMENT '封禁开始时间',
|
||||
`ban_expire` BIGINT NOT NULL DEFAULT 0 COMMENT '封禁到期时间',
|
||||
`vip_type` TINYINT NOT NULL DEFAULT 0 COMMENT 'VIP类型 1=非会员 2=月度会员 3=年度会员 4=体验会员',
|
||||
`vip_start` BIGINT NOT NULL DEFAULT 0 COMMENT 'VIP开始时间',
|
||||
`vip_expire` BIGINT NOT NULL DEFAULT 0 COMMENT 'VIP到期时间',
|
||||
`last_login_time` BIGINT NOT NULL DEFAULT 0 COMMENT '最近登录时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_wx_open_id` (`wx_open_id`),
|
||||
KEY `idx_users_user_uuid` (`user_uuid`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户表';
|
||||
|
||||
-- ============================================================
|
||||
-- 二、衣物
|
||||
-- ============================================================
|
||||
CREATE TABLE `clothes` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`clothes_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣物UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`category_main` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣物大类 上装/下装/连衣裙/外套/鞋/配饰',
|
||||
`category_sub` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣物小类 T恤/牛仔裤/运动鞋等',
|
||||
`style` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '风格',
|
||||
`color` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '主颜色',
|
||||
`color_sub` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '副颜色',
|
||||
`thickness` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '厚度',
|
||||
`version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '版型 宽松/修身/阔腿/紧身/常规合身',
|
||||
`material` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '材质 牛仔布/针织/皮质/纯棉/羊毛等',
|
||||
`pattern` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '花纹',
|
||||
`sleeve_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '袖型',
|
||||
`clothes_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣长类型 短款/常规/中长/长款',
|
||||
`neckline_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '领型',
|
||||
`formality` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '正式程度 1-5',
|
||||
`occasions` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '适用场合',
|
||||
`visual_impact` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '视觉冲击力 1-5',
|
||||
`name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '名称',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`processed_image` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '处理后图片存储位置 相对路径',
|
||||
`original_image_path` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '待处理原图本地路径(任务恢复用)',
|
||||
`image_size` BIGINT NOT NULL DEFAULT 0 COMMENT '图片大小 单位:kb',
|
||||
`image_process_status` TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态 1=抠图中 2=分类中 3=排队中 4=成功 5=失败',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_clothes_uuid` (`clothes_uuid`),
|
||||
KEY `idx_clothes_user_category` (`user_id`, `category_main`),
|
||||
KEY `idx_clothes_user_status` (`user_id`, `image_process_status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣物表';
|
||||
|
||||
-- ============================================================
|
||||
-- 三、穿搭
|
||||
-- ============================================================
|
||||
CREATE TABLE `outfits` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`outfit_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '穿搭UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`outfit_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '穿搭名称',
|
||||
`outfit_desc` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '穿搭描述',
|
||||
`scene` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '适合场景',
|
||||
`source` TINYINT NOT NULL DEFAULT 0 COMMENT '来源 1=用户创建 2=AI推荐',
|
||||
`is_public` TINYINT NOT NULL DEFAULT 0 COMMENT '是否公开 1=不公开 2=公开',
|
||||
`audit_status` TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态 1=待审核 2=审核通过 3=审核不通过',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=正常 2=封禁 3=待用户保存',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_outfits_uuid` (`outfit_uuid`),
|
||||
KEY `idx_outfits_user_status` (`user_id`, `status`),
|
||||
KEY `idx_outfits_public` (`is_public`, `audit_status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭表';
|
||||
|
||||
CREATE TABLE `outfit_items` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`outfit_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||
`clothes_id` BIGINT NOT NULL DEFAULT 0 COMMENT '衣物ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_outfit_items_outfit` (`outfit_id`),
|
||||
KEY `idx_outfit_items_clothes` (`clothes_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭明细表';
|
||||
|
||||
-- ============================================================
|
||||
-- 四、衣柜分享
|
||||
-- ============================================================
|
||||
CREATE TABLE `closet_shares` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`share_type` TINYINT NOT NULL DEFAULT 0 COMMENT '分享类型 1=部分衣物分享 2=全量快照分享',
|
||||
`expire_time` BIGINT NOT NULL DEFAULT 0 COMMENT '过期时间',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '分享状态 1=正常 2=用户取消 3=已过期 4=管理员封禁',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_closet_shares_uuid` (`share_uuid`),
|
||||
KEY `idx_closet_shares_user_status` (`user_id`, `status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享表';
|
||||
|
||||
CREATE TABLE `closet_share_items` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`clothes_id` BIGINT NOT NULL DEFAULT 0 COMMENT '衣物ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_closet_share_items_share` (`share_uuid`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享明细表';
|
||||
|
||||
-- 衣柜分享接收人关联表:记录"谁收到了谁的分享"(访问即视为收到,一对多、幂等)。
|
||||
CREATE TABLE `closet_share_receivers` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`receiver_user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收人用户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_closet_share_receiver` (`share_uuid`, `receiver_user_id`),
|
||||
KEY `idx_closet_share_receivers_user` (`receiver_user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享接收人关联表';
|
||||
|
||||
-- ============================================================
|
||||
-- 五、穿搭分享
|
||||
-- ============================================================
|
||||
CREATE TABLE `outfit_shares` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`outfit_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '分享状态 1=正常 2=用户取消',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_outfit_shares_uuid` (`share_uuid`),
|
||||
KEY `idx_outfit_shares_user_status` (`user_id`, `status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭分享表';
|
||||
|
||||
CREATE TABLE `outfit_share_receivers` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`receiver_user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收人用户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_outfit_share_receiver` (`share_uuid`, `receiver_user_id`),
|
||||
KEY `idx_outfit_share_receivers_user` (`receiver_user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭分享接收人关联表';
|
||||
|
||||
-- ============================================================
|
||||
-- 六、商品与订单
|
||||
-- ============================================================
|
||||
CREATE TABLE `goods` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`goods_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商品编码',
|
||||
`goods_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '商品名称',
|
||||
`goods_type` TINYINT NOT NULL DEFAULT 0 COMMENT '商品类型 1=会员包 2=上传包 3=AI推荐包 4=衣柜分享包 5=搭配分享包',
|
||||
`price` BIGINT NOT NULL DEFAULT 0 COMMENT '商品原价(分)',
|
||||
`discount_price` BIGINT NOT NULL DEFAULT 0 COMMENT '商品售价(分)',
|
||||
`member_days` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送会员天数',
|
||||
`cos_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送总上传额度',
|
||||
`ai_recommend_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送AI推荐额度',
|
||||
`closet_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送衣柜分享额度',
|
||||
`outfit_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送搭配分享额度',
|
||||
`valid_days` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送额度有效期天数(加量包用;会员包为 0 表示跟随 VIP)',
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '商品描述',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=上架 2=下架',
|
||||
`product_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '虚拟支付道具ID(空表示未发布)',
|
||||
`product_status` TINYINT NOT NULL DEFAULT 0 COMMENT '虚拟支付道具发布状态 0=未发布 1=发布中 2=已发布 3=发布失败',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_goods_code` (`goods_code`),
|
||||
KEY `idx_goods_type_status` (`goods_type`, `status`),
|
||||
KEY `idx_goods_product_status` (`product_status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '商品表';
|
||||
|
||||
CREATE TABLE `orders` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '下单用户ID',
|
||||
`goods_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联商品ID',
|
||||
`goods_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '商品名称快照',
|
||||
`unit_price` BIGINT NOT NULL DEFAULT 0 COMMENT '商品单价(分)',
|
||||
`total_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '订单总金额(分)',
|
||||
`pay_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '实付金额(分)',
|
||||
`pay_channel` TINYINT NOT NULL DEFAULT 0 COMMENT '支付渠道 1=微信支付 2=微信虚拟支付',
|
||||
`order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款',
|
||||
`transaction_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信支付流水号',
|
||||
`member_days` BIGINT NOT NULL DEFAULT 0 COMMENT '购买的会员天数(快照)',
|
||||
`goods_type` TINYINT NOT NULL DEFAULT 0 COMMENT '商品类型快照',
|
||||
`quota_valid_days` BIGINT NOT NULL DEFAULT 0 COMMENT '额度有效期天数快照',
|
||||
`cos_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送上传额度(快照)',
|
||||
`ai_recommend_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送AI推荐额度(快照)',
|
||||
`closet_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送衣柜分享额度(快照)',
|
||||
`outfit_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送搭配分享额度(快照)',
|
||||
`expire_time` BIGINT NOT NULL DEFAULT 0 COMMENT '支付过期时间',
|
||||
`pay_time` BIGINT NOT NULL DEFAULT 0 COMMENT '支付完成时间',
|
||||
`refund_time` BIGINT NOT NULL DEFAULT 0 COMMENT '退款时间',
|
||||
`refund_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '退款金额(分)',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '订单备注',
|
||||
`callback_raw` TEXT COMMENT '支付回调原始报文',
|
||||
`xpay_product_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '虚拟支付道具ID(快照,pay_channel=2 时使用)',
|
||||
`xpay_platform` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '虚拟支付平台 android/ios/harmony/windows',
|
||||
`xpay_env` TINYINT NOT NULL DEFAULT 0 COMMENT '虚拟支付环境 0=现网 1=沙箱',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_order_no` (`order_no`),
|
||||
KEY `idx_orders_user_status` (`user_id`, `order_status`),
|
||||
KEY `idx_orders_expire` (`order_status`, `expire_time`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '订单表';
|
||||
|
||||
-- ============================================================
|
||||
-- 七、额度
|
||||
-- ============================================================
|
||||
CREATE TABLE `user_quotas` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`quota_type` TINYINT NOT NULL DEFAULT 0 COMMENT '额度类型 1=cos上传 2=AI推荐 3=社区分享 4=衣柜分享',
|
||||
`total` BIGINT NOT NULL DEFAULT 0 COMMENT '总配额',
|
||||
`used` BIGINT NOT NULL DEFAULT 0 COMMENT '已用量',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_quota` (`user_id`, `quota_type`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户额度表';
|
||||
|
||||
CREATE TABLE `user_daily_usages` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`stat_date` BIGINT NOT NULL DEFAULT 0 COMMENT '统计日期(YYYYMMDD)',
|
||||
`ai_recommend_count` BIGINT NOT NULL DEFAULT 0 COMMENT '当日AI推荐次数',
|
||||
`outfit_share_count` BIGINT NOT NULL DEFAULT 0 COMMENT '当日社区分享次数',
|
||||
`closet_share_count` BIGINT NOT NULL DEFAULT 0 COMMENT '当日衣柜分享次数',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_daily_usage_user_date` (`user_id`, `stat_date`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户每日用量统计表';
|
||||
|
||||
-- 额度凭证表:承载有有效期的赠送额度(会员包/加量包/广告激励)。
|
||||
CREATE TABLE `quota_grants` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`quota_type` TINYINT NOT NULL DEFAULT 0 COMMENT '额度类型 1=cos上传 2=AI推荐 3=社区分享 4=衣柜分享',
|
||||
`amount` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送总量',
|
||||
`used` BIGINT NOT NULL DEFAULT 0 COMMENT '已用量(Cos 永久额度扣减;AI/社区/衣柜 每日额度恒为 0)',
|
||||
`source` TINYINT NOT NULL DEFAULT 0 COMMENT '来源 1=会员包 2=加量包 3=广告激励',
|
||||
`expire_time` BIGINT NOT NULL DEFAULT 0 COMMENT '到期时间(秒,0 表示跟随 VIP 有效期)',
|
||||
`ref_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联ID(订单号/广告记录ID)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_grant_ref_type` (`ref_id`, `quota_type`),
|
||||
KEY `idx_grant_user_type_expire` (`user_id`, `quota_type`, `expire_time`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '额度凭证表(有有效期赠送额度)';
|
||||
|
||||
CREATE TABLE `quota_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`quota_flow_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '配额流水UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`change_target` TINYINT NOT NULL DEFAULT 0 COMMENT '调整额度类型 1=配额调整 2=使用',
|
||||
`change_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更类型 1=购买获赠 2=活动赠送 3=上传消耗 4=删除回退 5=人工调整 6=退款回收',
|
||||
`change_num` BIGINT NOT NULL DEFAULT 0 COMMENT '变更数量(正数增加,负数减少)',
|
||||
`old_value` BIGINT NOT NULL DEFAULT 0 COMMENT '变更前数值',
|
||||
`new_value` BIGINT NOT NULL DEFAULT 0 COMMENT '变更后数值',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联订单号(购买/退款类);其余场景写流水UUID保证幂等',
|
||||
`quota_type` TINYINT NOT NULL DEFAULT 0 COMMENT '关联业务类型 1=cos 2=AI 3=社区分享 4=衣柜分享',
|
||||
`related_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联业务ID',
|
||||
`reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '变更原因/备注',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_quota_flow_idem` (`order_no`, `quota_type`, `change_type`),
|
||||
KEY `idx_quota_flow_user` (`user_id`),
|
||||
KEY `idx_quota_flow_uuid` (`quota_flow_uuid`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '配额流水日志表';
|
||||
|
||||
-- ============================================================
|
||||
-- 八、权益发放与回收(补偿任务)
|
||||
-- ============================================================
|
||||
CREATE TABLE `benefit_grant_tasks` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号',
|
||||
`goods_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联商品ID',
|
||||
`benefit_type` TINYINT NOT NULL DEFAULT 0 COMMENT '权益类型 1=vip 2=cos 3=ai 4=closet_share 5=outfit_share',
|
||||
`amount` BIGINT NOT NULL DEFAULT 0 COMMENT '发放数量',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0=待发放 1=已发放 2=发放失败',
|
||||
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数',
|
||||
`max_retry` INT NOT NULL DEFAULT 0 COMMENT '最大重试次数',
|
||||
`last_error` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '最近一次失败原因',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_benefit_task_idem` (`order_no`, `benefit_type`),
|
||||
KEY `idx_benefit_task_status` (`status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '权益发放补偿任务表';
|
||||
|
||||
CREATE TABLE `benefit_revoke_tasks` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0=待回收 1=已回收 2=回收失败',
|
||||
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数',
|
||||
`max_retry` INT NOT NULL DEFAULT 10 COMMENT '最大重试次数',
|
||||
`last_error` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '最近一次失败原因',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_benefit_revoke_order` (`order_no`),
|
||||
KEY `idx_benefit_revoke_status` (`status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '退款权益回收任务';
|
||||
|
||||
-- ============================================================
|
||||
-- 九、资金与调用流水
|
||||
-- ============================================================
|
||||
CREATE TABLE `refund_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`order_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联订单ID',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号(冗余,方便查询)',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID(冗余,方便按用户查)',
|
||||
`refund_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户退款单号',
|
||||
`wechat_refund_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信退款单号',
|
||||
`refund_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '本次退款金额(分)',
|
||||
`refund_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '退款原因',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '退款状态 0=待处理 1=退款中 2=退款成功 3=退款失败',
|
||||
`apply_time` BIGINT NOT NULL DEFAULT 0 COMMENT '申请时间',
|
||||
`success_time` BIGINT NOT NULL DEFAULT 0 COMMENT '退款成功时间',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
`callback_raw` TEXT COMMENT '微信退款回调原始报文',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_refund_flow_no` (`refund_no`),
|
||||
KEY `idx_refund_flow_order` (`order_id`),
|
||||
KEY `idx_refund_flow_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '退款流水日志表';
|
||||
|
||||
CREATE TABLE `vip_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`change_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更类型 1=开通 2=续费 3=活动赠送 4=到期失效 5=人工调整 6=退款回收',
|
||||
`old_vip_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更前VIP类型 1=非会员 2=月度会员 3=年度会员 4=体验会员',
|
||||
`new_vip_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更后VIP类型 1=非会员 2=月度会员 3=年度会员 4=体验会员',
|
||||
`old_vip_expire` BIGINT NOT NULL DEFAULT 0 COMMENT '变更前到期时间(秒)',
|
||||
`new_vip_expire` BIGINT NOT NULL DEFAULT 0 COMMENT '变更后到期时间(秒)',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联订单号(购买类变更)',
|
||||
`source` TINYINT NOT NULL DEFAULT 0 COMMENT '变更来源 1=个人支付 2=活动赠送 3=系统 4=人工调整',
|
||||
`reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '变更原因/备注',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_vip_flow_idem` (`order_no`, `change_type`),
|
||||
KEY `idx_vip_flow_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '会员变更流水表';
|
||||
|
||||
CREATE TABLE `ai_recommend_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '请求唯一ID',
|
||||
`server_request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务请求唯一ID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`model_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '大模型版本',
|
||||
`prompt_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输入token数',
|
||||
`completion_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输出token数',
|
||||
`result_content` TEXT COMMENT '推荐结果结构化数据',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=成功 2=失败',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
`request_time` BIGINT NOT NULL DEFAULT 0 COMMENT '请求发起时间',
|
||||
`response_time` BIGINT NOT NULL DEFAULT 0 COMMENT '响应返回时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ai_recommend_request` (`request_id`),
|
||||
KEY `idx_ai_recommend_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'AI穿搭推荐流水表';
|
||||
|
||||
CREATE TABLE `ai_horoscope_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '请求唯一ID(幂等依据)',
|
||||
`zodiac` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '星座',
|
||||
`server_request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务请求唯一ID',
|
||||
`stat_date` BIGINT NOT NULL DEFAULT 0 COMMENT '运势日期(YYYYMMDD)',
|
||||
`model_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '大模型版本',
|
||||
`prompt_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输入token数',
|
||||
`completion_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输出token数',
|
||||
`result_content` TEXT COMMENT '生成结果结构化数据',
|
||||
`horoscope_id` BIGINT NOT NULL DEFAULT 0 COMMENT '生成的运势记录ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=成功 2=失败',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
`request_time` BIGINT NOT NULL DEFAULT 0 COMMENT '请求发起时间',
|
||||
`response_time` BIGINT NOT NULL DEFAULT 0 COMMENT '响应返回时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ai_horoscope_request` (`request_id`),
|
||||
KEY `idx_ai_horoscope_date_zodiac` (`stat_date`, `zodiac`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'AI运势生成流水表';
|
||||
|
||||
-- ============================================================
|
||||
-- 十、互动与通知
|
||||
-- ============================================================
|
||||
CREATE TABLE `collects` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`collect_type` TINYINT NOT NULL DEFAULT 0 COMMENT '业务类型 1=社区穿搭',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '业务目标ID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '收藏用户ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=已收藏 0=已取消',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_collect_user_target` (`collect_type`, `target_id`, `user_id`),
|
||||
KEY `idx_collects_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '收藏表';
|
||||
|
||||
CREATE TABLE `likes` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`like_type` TINYINT NOT NULL DEFAULT 0 COMMENT '业务类型 1=社区穿搭',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '业务目标ID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '点赞用户ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=已点赞 0=已取消',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_like_user_target` (`like_type`, `target_id`, `user_id`),
|
||||
KEY `idx_likes_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '点赞表';
|
||||
|
||||
CREATE TABLE `user_notifications` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`notification_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '消息UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收用户ID',
|
||||
`msg_type` TINYINT NOT NULL DEFAULT 0 COMMENT '消息类型 1=点赞通知 2=订单通知 3=系统通知',
|
||||
`sender_id` BIGINT NOT NULL DEFAULT 0 COMMENT '发送者ID 系统消息为0',
|
||||
`title` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '消息标题',
|
||||
`content` TEXT COMMENT '消息内容',
|
||||
`is_read` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已读 1=未读 2=已读',
|
||||
`read_time` BIGINT NOT NULL DEFAULT 0 COMMENT '阅读时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_notification_uuid` (`notification_uuid`),
|
||||
KEY `idx_notifications_user_read` (`user_id`, `is_read`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户通知表';
|
||||
|
||||
-- ============================================================
|
||||
-- 十一、运势与审核
|
||||
-- ============================================================
|
||||
CREATE TABLE `horoscopes` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`stat_date` BIGINT NOT NULL DEFAULT 0 COMMENT '运势日期(YYYYMMDD)',
|
||||
`zodiac` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '星座',
|
||||
`overall_score` TINYINT NOT NULL DEFAULT 0 COMMENT '综合运势 1-5分',
|
||||
`love_score` TINYINT NOT NULL DEFAULT 0 COMMENT '爱情运势 1-5分',
|
||||
`career_score` TINYINT NOT NULL DEFAULT 0 COMMENT '工作运势 1-5分',
|
||||
`wealth_score` TINYINT NOT NULL DEFAULT 0 COMMENT '财富运势 1-5分',
|
||||
`daily_text` TEXT COMMENT '今日运势详情',
|
||||
`lucky_color` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '幸运色',
|
||||
`lucky_number` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '幸运数字',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_horoscope_date_zodiac` (`stat_date`, `zodiac`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '星座运势表';
|
||||
|
||||
CREATE TABLE `closet_audit_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '衣柜分享ID',
|
||||
`audit_result` TINYINT NOT NULL DEFAULT 0 COMMENT '审核结果 1=通过 2=驳回',
|
||||
`reject_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '审核原因',
|
||||
`audit_time` BIGINT NOT NULL DEFAULT 0 COMMENT '审核时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_closet_audit_target` (`target_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享审核日志表';
|
||||
|
||||
CREATE TABLE `outfit_audit_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||
`audit_result` TINYINT NOT NULL DEFAULT 0 COMMENT '审核结果 1=通过 2=驳回',
|
||||
`reject_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '审核原因',
|
||||
`audit_time` BIGINT NOT NULL DEFAULT 0 COMMENT '审核时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_outfit_audit_target` (`target_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭审核日志表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `clothes`;
|
||||
@@ -1,36 +0,0 @@
|
||||
CREATE TABLE `clothes` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`clothes_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣物UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`category_main` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣物大类 上装/下装/连衣裙/外套/鞋/配饰',
|
||||
`category_sub` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣物小类 T恤/牛仔裤/运动鞋等',
|
||||
`style` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '风格',
|
||||
`color` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '主颜色',
|
||||
`color_sub` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '副颜色',
|
||||
`thickness` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '厚度',
|
||||
`version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '版型 宽松/修身/阔腿/紧身/常规合身',
|
||||
`material` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '材质 牛仔布/针织/皮质/纯棉/羊毛等',
|
||||
`pattern` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '花纹',
|
||||
`sleeve_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '袖型',
|
||||
`clothes_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '衣长类型 短款/常规/中长/长款',
|
||||
`neckline_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '领型',
|
||||
`formality` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '正式程度 1-5',
|
||||
`occasions` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '适用场合',
|
||||
`visual_impact` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '视觉冲击力 1-5',
|
||||
`name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '名称',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
|
||||
`processed_image` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '处理后图片存储位置 相对路径',
|
||||
`image_size` BIGINT NOT NULL DEFAULT 0 COMMENT '图片大小 单位:kb',
|
||||
`image_process_status` TINYINT NOT NULL DEFAULT 0 COMMENT '处理状态 1=抠图中 2=分类中 3=排队中 4=成功 5=失败',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_clothes_uuid` (`clothes_uuid`),
|
||||
KEY `idx_clothes_user_category` (`user_id`, `category_main`),
|
||||
KEY `idx_clothes_user_status` (`user_id`, `image_process_status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣物表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `outfit_items`;
|
||||
DROP TABLE IF EXISTS `outfits`;
|
||||
@@ -1,37 +0,0 @@
|
||||
CREATE TABLE `outfits` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`outfit_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '穿搭UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`outfit_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '穿搭名称',
|
||||
`outfit_desc` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '穿搭描述',
|
||||
`scene` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '适合场景',
|
||||
`source` TINYINT NOT NULL DEFAULT 0 COMMENT '来源 1=用户创建 2=AI推荐',
|
||||
`is_public` TINYINT NOT NULL DEFAULT 0 COMMENT '是否公开 1=不公开 2=公开',
|
||||
`audit_status` TINYINT NOT NULL DEFAULT 0 COMMENT '审核状态 1=待审核 2=审核通过 3=审核不通过',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=正常 2=封禁 3=待用户保存',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_outfits_uuid` (`outfit_uuid`),
|
||||
KEY `idx_outfits_user_status` (`user_id`, `status`),
|
||||
KEY `idx_outfits_public` (`is_public`, `audit_status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭表';
|
||||
|
||||
CREATE TABLE `outfit_items` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`outfit_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||
`clothes_id` BIGINT NOT NULL DEFAULT 0 COMMENT '衣物ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_outfit_items_outfit` (`outfit_id`),
|
||||
KEY `idx_outfit_items_clothes` (`clothes_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭明细表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `closet_share_items`;
|
||||
DROP TABLE IF EXISTS `closet_shares`;
|
||||
@@ -1,31 +0,0 @@
|
||||
CREATE TABLE `closet_shares` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`share_type` TINYINT NOT NULL DEFAULT 0 COMMENT '分享类型 1=部分衣物分享 2=全量快照分享',
|
||||
`expire_time` BIGINT NOT NULL DEFAULT 0 COMMENT '过期时间',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '分享状态 1=正常 2=用户取消 3=已过期 4=管理员封禁',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_closet_shares_uuid` (`share_uuid`),
|
||||
KEY `idx_closet_shares_user_status` (`user_id`, `status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享表';
|
||||
|
||||
CREATE TABLE `closet_share_items` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`clothes_id` BIGINT NOT NULL DEFAULT 0 COMMENT '衣物ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_closet_share_items_share` (`share_uuid`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享明细表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `user_daily_usages`;
|
||||
DROP TABLE IF EXISTS `user_quotas`;
|
||||
@@ -1,32 +0,0 @@
|
||||
CREATE TABLE `user_quotas` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`quota_type` TINYINT NOT NULL DEFAULT 0 COMMENT '额度类型 1=cos上传 2=AI推荐 3=社区分享 4=衣柜分享',
|
||||
`total` BIGINT NOT NULL DEFAULT 0 COMMENT '总配额',
|
||||
`used` BIGINT NOT NULL DEFAULT 0 COMMENT '已用量',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_user_quota` (`user_id`, `quota_type`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户额度表';
|
||||
|
||||
CREATE TABLE `user_daily_usages` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`stat_date` BIGINT NOT NULL DEFAULT 0 COMMENT '统计日期(YYYYMMDD)',
|
||||
`ai_recommend_count` BIGINT NOT NULL DEFAULT 0 COMMENT '当日AI推荐次数',
|
||||
`outfit_share_count` BIGINT NOT NULL DEFAULT 0 COMMENT '当日社区分享次数',
|
||||
`closet_share_count` BIGINT NOT NULL DEFAULT 0 COMMENT '当日衣柜分享次数',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_daily_usage_user_date` (`user_id`, `stat_date`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户每日用量统计表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `quota_flow_logs`;
|
||||
@@ -1,24 +0,0 @@
|
||||
CREATE TABLE `quota_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`quota_flow_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '配额流水UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`change_target` TINYINT NOT NULL DEFAULT 0 COMMENT '调整额度类型 1=配额调整 2=使用',
|
||||
`change_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更类型 1=购买获赠 2=活动赠送 3=上传消耗 4=删除回退 5=人工调整 6=退款回收',
|
||||
`change_num` BIGINT NOT NULL DEFAULT 0 COMMENT '变更数量(正数增加,负数减少)',
|
||||
`old_value` BIGINT NOT NULL DEFAULT 0 COMMENT '变更前数值',
|
||||
`new_value` BIGINT NOT NULL DEFAULT 0 COMMENT '变更后数值',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联订单号(购买/退款类);其余场景写流水UUID保证幂等',
|
||||
`quota_type` TINYINT NOT NULL DEFAULT 0 COMMENT '关联业务类型 1=cos 2=AI 3=社区分享 4=衣柜分享',
|
||||
`related_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联业务ID',
|
||||
`reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '变更原因/备注',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_quota_flow_idem` (`order_no`, `quota_type`, `change_type`),
|
||||
KEY `idx_quota_flow_user` (`user_id`),
|
||||
KEY `idx_quota_flow_uuid` (`quota_flow_uuid`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '配额流水日志表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `goods`;
|
||||
@@ -1,28 +0,0 @@
|
||||
CREATE TABLE `goods` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`goods_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商品编码',
|
||||
`goods_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '商品名称',
|
||||
`goods_type` TINYINT NOT NULL DEFAULT 0 COMMENT '商品类型 1=会员包 2=上传包 3=AI推荐包 4=衣柜分享包 5=搭配分享包',
|
||||
`price` BIGINT NOT NULL DEFAULT 0 COMMENT '商品原价(分)',
|
||||
`discount_price` BIGINT NOT NULL DEFAULT 0 COMMENT '商品售价(分)',
|
||||
`member_days` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送会员天数',
|
||||
`cos_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送总上传额度',
|
||||
`ai_recommend_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送AI推荐额度',
|
||||
`closet_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送衣柜分享额度',
|
||||
`outfit_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送搭配分享额度',
|
||||
`valid_days` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送额度有效期天数(加量包用;会员包为 0 表示跟随 VIP)',
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '商品描述',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=上架 2=下架',
|
||||
`product_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '虚拟支付道具ID(空表示未发布)',
|
||||
`product_status` TINYINT NOT NULL DEFAULT 0 COMMENT '虚拟支付道具发布状态 0=未发布 1=发布中 2=已发布 3=发布失败',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_goods_code` (`goods_code`),
|
||||
KEY `idx_goods_type_status` (`goods_type`, `status`),
|
||||
KEY `idx_goods_product_status` (`product_status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '商品表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `orders`;
|
||||
@@ -1,37 +0,0 @@
|
||||
CREATE TABLE `orders` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '下单用户ID',
|
||||
`goods_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联商品ID',
|
||||
`goods_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '商品名称快照',
|
||||
`unit_price` BIGINT NOT NULL DEFAULT 0 COMMENT '商品单价(分)',
|
||||
`total_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '订单总金额(分)',
|
||||
`pay_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '实付金额(分)',
|
||||
`pay_channel` TINYINT NOT NULL DEFAULT 0 COMMENT '支付渠道 1=微信支付',
|
||||
`order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态 1=待支付 2=已支付 3=已取消 4=已过期 5=已退款',
|
||||
`transaction_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信支付流水号',
|
||||
`member_days` BIGINT NOT NULL DEFAULT 0 COMMENT '购买的会员天数(快照)',
|
||||
`cos_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送上传额度(快照)',
|
||||
`ai_recommend_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送AI推荐额度(快照)',
|
||||
`closet_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送衣柜分享额度(快照)',
|
||||
`outfit_share_quota` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送搭配分享额度(快照)',
|
||||
`expire_time` BIGINT NOT NULL DEFAULT 0 COMMENT '支付过期时间',
|
||||
`pay_time` BIGINT NOT NULL DEFAULT 0 COMMENT '支付完成时间',
|
||||
`refund_time` BIGINT NOT NULL DEFAULT 0 COMMENT '退款时间',
|
||||
`refund_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '退款金额(分)',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '订单备注',
|
||||
`callback_raw` TEXT COMMENT '支付回调原始报文',
|
||||
`xpay_product_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '虚拟支付道具ID(快照,pay_channel=2 时使用)',
|
||||
`xpay_platform` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '虚拟支付平台 android/ios/harmony/windows',
|
||||
`xpay_env` TINYINT NOT NULL DEFAULT 0 COMMENT '虚拟支付环境 0=现网 1=沙箱',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_order_no` (`order_no`),
|
||||
KEY `idx_orders_user_status` (`user_id`, `order_status`),
|
||||
KEY `idx_orders_expire` (`order_status`, `expire_time`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '订单表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `refund_flow_logs`;
|
||||
@@ -1,25 +0,0 @@
|
||||
CREATE TABLE `refund_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`order_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联订单ID',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号(冗余,方便查询)',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID(冗余,方便按用户查)',
|
||||
`refund_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户退款单号',
|
||||
`wechat_refund_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信退款单号',
|
||||
`refund_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '本次退款金额(分)',
|
||||
`refund_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '退款原因',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '退款状态 0=待处理 1=退款中 2=退款成功 3=退款失败',
|
||||
`apply_time` BIGINT NOT NULL DEFAULT 0 COMMENT '申请时间',
|
||||
`success_time` BIGINT NOT NULL DEFAULT 0 COMMENT '退款成功时间',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
`callback_raw` TEXT COMMENT '微信退款回调原始报文',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_refund_flow_no` (`refund_no`),
|
||||
KEY `idx_refund_flow_order` (`order_id`),
|
||||
KEY `idx_refund_flow_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '退款流水日志表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `vip_flow_logs`;
|
||||
@@ -1,21 +0,0 @@
|
||||
CREATE TABLE `vip_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`change_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更类型 1=开通 2=续费 3=活动赠送 4=到期失效 5=人工调整 6=退款回收',
|
||||
`old_vip_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更前VIP类型 1=非会员 2=月度会员 3=年度会员 4=体验会员',
|
||||
`new_vip_type` TINYINT NOT NULL DEFAULT 0 COMMENT '变更后VIP类型 1=非会员 2=月度会员 3=年度会员 4=体验会员',
|
||||
`old_vip_expire` BIGINT NOT NULL DEFAULT 0 COMMENT '变更前到期时间(秒)',
|
||||
`new_vip_expire` BIGINT NOT NULL DEFAULT 0 COMMENT '变更后到期时间(秒)',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联订单号(购买类变更)',
|
||||
`source` TINYINT NOT NULL DEFAULT 0 COMMENT '变更来源 1=个人支付 2=活动赠送 3=系统 4=人工调整',
|
||||
`reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '变更原因/备注',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_vip_flow_idem` (`order_no`, `change_type`),
|
||||
KEY `idx_vip_flow_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '会员变更流水表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `benefit_grant_tasks`;
|
||||
@@ -1,21 +0,0 @@
|
||||
CREATE TABLE `benefit_grant_tasks` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`order_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商户订单号',
|
||||
`goods_id` BIGINT NOT NULL DEFAULT 0 COMMENT '关联商品ID',
|
||||
`benefit_type` TINYINT NOT NULL DEFAULT 0 COMMENT '权益类型 1=vip 2=cos 3=ai 4=closet_share 5=outfit_share',
|
||||
`amount` BIGINT NOT NULL DEFAULT 0 COMMENT '发放数量',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0=待发放 1=已发放 2=发放失败',
|
||||
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '已重试次数',
|
||||
`max_retry` INT NOT NULL DEFAULT 0 COMMENT '最大重试次数',
|
||||
`last_error` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '最近一次失败原因',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_benefit_task_idem` (`order_no`, `benefit_type`),
|
||||
KEY `idx_benefit_task_status` (`status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '权益发放补偿任务表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `ai_horoscope_flow_logs`;
|
||||
DROP TABLE IF EXISTS `ai_recommend_flow_logs`;
|
||||
@@ -1,49 +0,0 @@
|
||||
CREATE TABLE `ai_recommend_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '请求唯一ID',
|
||||
`server_request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务请求唯一ID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`model_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '大模型版本',
|
||||
`prompt_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输入token数',
|
||||
`completion_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输出token数',
|
||||
`result_content` TEXT COMMENT '推荐结果结构化数据',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=成功 2=失败',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
`request_time` BIGINT NOT NULL DEFAULT 0 COMMENT '请求发起时间',
|
||||
`response_time` BIGINT NOT NULL DEFAULT 0 COMMENT '响应返回时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ai_recommend_request` (`request_id`),
|
||||
KEY `idx_ai_recommend_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'AI穿搭推荐流水表';
|
||||
|
||||
CREATE TABLE `ai_horoscope_flow_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '请求唯一ID(幂等依据)',
|
||||
`zodiac` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '星座',
|
||||
`server_request_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务请求唯一ID',
|
||||
`stat_date` BIGINT NOT NULL DEFAULT 0 COMMENT '运势日期(YYYYMMDD)',
|
||||
`model_version` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '大模型版本',
|
||||
`prompt_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输入token数',
|
||||
`completion_tokens` BIGINT NOT NULL DEFAULT 0 COMMENT '输出token数',
|
||||
`result_content` TEXT COMMENT '生成结果结构化数据',
|
||||
`horoscope_id` BIGINT NOT NULL DEFAULT 0 COMMENT '生成的运势记录ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=成功 2=失败',
|
||||
`fail_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
|
||||
`request_time` BIGINT NOT NULL DEFAULT 0 COMMENT '请求发起时间',
|
||||
`response_time` BIGINT NOT NULL DEFAULT 0 COMMENT '响应返回时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ai_horoscope_request` (`request_id`),
|
||||
KEY `idx_ai_horoscope_date_zodiac` (`stat_date`, `zodiac`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'AI运势生成流水表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `likes`;
|
||||
DROP TABLE IF EXISTS `collects`;
|
||||
@@ -1,33 +0,0 @@
|
||||
CREATE TABLE `collects` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`collect_type` TINYINT NOT NULL DEFAULT 0 COMMENT '业务类型 1=社区穿搭',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '业务目标ID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '收藏用户ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=已收藏 0=已取消',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_collect_user_target` (`collect_type`, `target_id`, `user_id`),
|
||||
KEY `idx_collects_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '收藏表';
|
||||
|
||||
CREATE TABLE `likes` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`like_type` TINYINT NOT NULL DEFAULT 0 COMMENT '业务类型 1=社区穿搭',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '业务目标ID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '点赞用户ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态 1=已点赞 0=已取消',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_like_user_target` (`like_type`, `target_id`, `user_id`),
|
||||
KEY `idx_likes_user` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '点赞表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `user_notifications`;
|
||||
@@ -1,20 +0,0 @@
|
||||
CREATE TABLE `user_notifications` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`notification_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '消息UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收用户ID',
|
||||
`msg_type` TINYINT NOT NULL DEFAULT 0 COMMENT '消息类型 1=点赞通知 2=订单通知 3=系统通知',
|
||||
`sender_id` BIGINT NOT NULL DEFAULT 0 COMMENT '发送者ID 系统消息为0',
|
||||
`title` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '消息标题',
|
||||
`content` TEXT COMMENT '消息内容',
|
||||
`is_read` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已读 1=未读 2=已读',
|
||||
`read_time` BIGINT NOT NULL DEFAULT 0 COMMENT '阅读时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_notification_uuid` (`notification_uuid`),
|
||||
KEY `idx_notifications_user_read` (`user_id`, `is_read`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户通知表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `horoscopes`;
|
||||
@@ -1,20 +0,0 @@
|
||||
CREATE TABLE `horoscopes` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`stat_date` BIGINT NOT NULL DEFAULT 0 COMMENT '运势日期(YYYYMMDD)',
|
||||
`zodiac` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '星座',
|
||||
`overall_score` TINYINT NOT NULL DEFAULT 0 COMMENT '综合运势 1-5分',
|
||||
`love_score` TINYINT NOT NULL DEFAULT 0 COMMENT '爱情运势 1-5分',
|
||||
`career_score` TINYINT NOT NULL DEFAULT 0 COMMENT '工作运势 1-5分',
|
||||
`wealth_score` TINYINT NOT NULL DEFAULT 0 COMMENT '财富运势 1-5分',
|
||||
`daily_text` TEXT COMMENT '今日运势详情',
|
||||
`lucky_color` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '幸运色',
|
||||
`lucky_number` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '幸运数字',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_horoscope_date_zodiac` (`stat_date`, `zodiac`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '星座运势表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `outfit_audit_logs`;
|
||||
DROP TABLE IF EXISTS `closet_audit_logs`;
|
||||
@@ -1,31 +0,0 @@
|
||||
CREATE TABLE `closet_audit_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '衣柜分享ID',
|
||||
`audit_result` TINYINT NOT NULL DEFAULT 0 COMMENT '审核结果 1=通过 2=驳回',
|
||||
`reject_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '审核原因',
|
||||
`audit_time` BIGINT NOT NULL DEFAULT 0 COMMENT '审核时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_closet_audit_target` (`target_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享审核日志表';
|
||||
|
||||
CREATE TABLE `outfit_audit_logs` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`target_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||
`audit_result` TINYINT NOT NULL DEFAULT 0 COMMENT '审核结果 1=通过 2=驳回',
|
||||
`reject_reason` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '审核原因',
|
||||
`audit_time` BIGINT NOT NULL DEFAULT 0 COMMENT '审核时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_outfit_audit_target` (`target_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭审核日志表';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `quota_grants`;
|
||||
@@ -1,20 +0,0 @@
|
||||
-- 额度凭证表:承载有有效期的赠送额度(会员包/加量包/广告激励)。
|
||||
CREATE TABLE `quota_grants` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`quota_type` TINYINT NOT NULL DEFAULT 0 COMMENT '额度类型 1=cos上传 2=AI推荐 3=社区分享 4=衣柜分享',
|
||||
`amount` BIGINT NOT NULL DEFAULT 0 COMMENT '赠送总量',
|
||||
`used` BIGINT NOT NULL DEFAULT 0 COMMENT '已用量(Cos 永久额度扣减;AI/社区/衣柜 每日额度恒为 0)',
|
||||
`source` TINYINT NOT NULL DEFAULT 0 COMMENT '来源 1=会员包 2=加量包 3=广告激励',
|
||||
`expire_time` BIGINT NOT NULL DEFAULT 0 COMMENT '到期时间(秒,0 表示跟随 VIP 有效期)',
|
||||
`ref_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '关联ID(订单号/广告记录ID)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_grant_ref_type` (`ref_id`, `quota_type`),
|
||||
KEY `idx_grant_user_type_expire` (`user_id`, `quota_type`, `expire_time`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '额度凭证表(有有效期赠送额度)';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS `closet_share_receivers`;
|
||||
@@ -1,15 +0,0 @@
|
||||
-- 衣柜分享接收人关联表:记录"谁收到了谁的分享"(访问即视为收到,一对多、幂等)。
|
||||
CREATE TABLE `closet_share_receivers` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`receiver_user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收人用户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_closet_share_receiver` (`share_uuid`, `receiver_user_id`),
|
||||
KEY `idx_closet_share_receivers_user` (`receiver_user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '衣柜分享接收人关联表';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `outfit_share_receivers`;
|
||||
DROP TABLE IF EXISTS `outfit_shares`;
|
||||
@@ -1,31 +0,0 @@
|
||||
CREATE TABLE `outfit_shares` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '所属用户ID',
|
||||
`outfit_id` BIGINT NOT NULL DEFAULT 0 COMMENT '穿搭ID',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '分享状态 1=正常 2=用户取消',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_outfit_shares_uuid` (`share_uuid`),
|
||||
KEY `idx_outfit_shares_user_status` (`user_id`, `status`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭分享表';
|
||||
|
||||
CREATE TABLE `outfit_share_receivers` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT '删除时间(0 表示未删除)',
|
||||
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`updated_by` BIGINT NOT NULL DEFAULT 0 COMMENT '更新人',
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0 COMMENT '删除人',
|
||||
`share_uuid` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分享UUID',
|
||||
`receiver_user_id` BIGINT NOT NULL DEFAULT 0 COMMENT '接收人用户ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_outfit_share_receiver` (`share_uuid`, `receiver_user_id`),
|
||||
KEY `idx_outfit_share_receivers_user` (`receiver_user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '穿搭分享接收人关联表';
|
||||
@@ -1,3 +0,0 @@
|
||||
DROP TABLE IF EXISTS `benefit_revoke_tasks`;
|
||||
ALTER TABLE `clothes` DROP COLUMN `original_image_path`;
|
||||
ALTER TABLE `orders` DROP COLUMN `quota_valid_days`, DROP COLUMN `goods_type`;
|
||||
@@ -1,14 +0,0 @@
|
||||
ALTER TABLE `orders`
|
||||
ADD COLUMN `goods_type` TINYINT NOT NULL DEFAULT 0 COMMENT '商品类型快照' AFTER `member_days`,
|
||||
ADD COLUMN `quota_valid_days` BIGINT NOT NULL DEFAULT 0 COMMENT '额度有效期天数快照' AFTER `goods_type`;
|
||||
UPDATE `orders` o JOIN `goods` g ON g.id = o.goods_id
|
||||
SET o.goods_type = g.goods_type, o.quota_valid_days = g.valid_days WHERE o.goods_type = 0;
|
||||
ALTER TABLE `clothes` ADD COLUMN `original_image_path` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '待处理原图本地路径' AFTER `processed_image`;
|
||||
CREATE TABLE `benefit_revoke_tasks` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT, `created_at` BIGINT NOT NULL DEFAULT 0, `updated_at` BIGINT NOT NULL DEFAULT 0,
|
||||
`deleted_at` BIGINT NOT NULL DEFAULT 0, `created_by` BIGINT NOT NULL DEFAULT 0, `updated_by` BIGINT NOT NULL DEFAULT 0,
|
||||
`deleted_by` BIGINT NOT NULL DEFAULT 0, `order_no` VARCHAR(64) NOT NULL DEFAULT '', `user_id` BIGINT NOT NULL DEFAULT 0,
|
||||
`status` TINYINT NOT NULL DEFAULT 0, `retry_count` INT NOT NULL DEFAULT 0, `max_retry` INT NOT NULL DEFAULT 10,
|
||||
`last_error` VARCHAR(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_benefit_revoke_order` (`order_no`),
|
||||
KEY `idx_benefit_revoke_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款权益回收任务';
|
||||
@@ -30,9 +30,10 @@ type RecommendLLMResponse struct {
|
||||
}
|
||||
|
||||
// GenerateOutfitRecommendation 调用混元生成今日穿搭推荐。
|
||||
func (c *Client) GenerateOutfitRecommendation(ctx context.Context, zodiac string, fortune *FortuneItem, clothes []mysqlmodel.Clothes) (*RecommendLLMResponse, error) {
|
||||
// scene 为用户自定义场景(前端 scene_type=2 时传入),为空表示由模型自行判断。
|
||||
func (c *Client) GenerateOutfitRecommendation(ctx context.Context, zodiac string, fortune *FortuneItem, clothes []mysqlmodel.Clothes, scene string) (*RecommendLLMResponse, error) {
|
||||
messages := []Message{
|
||||
{Role: "user", Content: buildRecommendPrompt(zodiac, fortune, clothes)},
|
||||
{Role: "user", Content: buildRecommendPrompt(zodiac, fortune, clothes, scene)},
|
||||
}
|
||||
|
||||
responseFormat := &ResponseFormat{
|
||||
@@ -67,9 +68,16 @@ func (c *Client) GenerateOutfitRecommendation(ctx context.Context, zodiac string
|
||||
}
|
||||
|
||||
// buildRecommendPrompt 构造穿搭推荐 prompt。
|
||||
func buildRecommendPrompt(zodiac string, fortune *FortuneItem, clothes []mysqlmodel.Clothes) string {
|
||||
// scene 为用户自定义场景(已做注入清洗):非空时作为硬约束写入 prompt,并指示模型在 scene 字段回填;
|
||||
// 为空时整段不出现,保持「由模型自行判断场景」的原行为。
|
||||
func buildRecommendPrompt(zodiac string, fortune *FortuneItem, clothes []mysqlmodel.Clothes, scene string) string {
|
||||
clothesText := FormatClothesInfo(clothes)
|
||||
|
||||
sceneSection := ""
|
||||
if s := sanitizePromptField(scene); s != "" {
|
||||
sceneSection = fmt.Sprintf("\n## 用户指定场景\n用户希望这套搭配用于「%s」,请优先围绕该场景挑选单品,并在 scene 字段回填该场景。\n", s)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`你是一位专业的时尚搭配师。请根据用户今天的星座运势和衣橱中的衣物,为用户推荐一套今日穿搭。
|
||||
|
||||
## 用户信息
|
||||
@@ -82,7 +90,7 @@ func buildRecommendPrompt(zodiac string, fortune *FortuneItem, clothes []mysqlmo
|
||||
|
||||
## 用户衣橱
|
||||
%s
|
||||
|
||||
%s
|
||||
## 搭配规则
|
||||
1. 所有推荐单品必须严格选自用户衣橱,clothes_id 与 clothes_uuid 必须与衣橱中存在的衣物一一对应,不得虚构
|
||||
2. 先识别衣橱中包含的有效品类,仅统计以下 6 类:上装、下装、连衣裙、外套、鞋、配饰:
|
||||
@@ -113,7 +121,7 @@ func buildRecommendPrompt(zodiac string, fortune *FortuneItem, clothes []mysqlmo
|
||||
- items 数组长度由衣橱中存在的品类数量决定,不存在的品类不写入数组
|
||||
- clothes_id 必须为整数,且必须是衣橱中真实存在的 ID
|
||||
- clothes_uuid 必须从「用户衣橱」信息中精确复制,且与 clothes_id 一一对应,严禁编造或改动 UUID`,
|
||||
zodiac, fortune.Overall, fortune.Love, fortune.Work, fortune.Money, fortune.Description, clothesText)
|
||||
zodiac, fortune.Overall, fortune.Love, fortune.Work, fortune.Money, fortune.Description, clothesText, sceneSection)
|
||||
}
|
||||
|
||||
// buildRecommendSchema 构造 JSON Schema 要求大模型结构化输出。
|
||||
|
||||
@@ -283,12 +283,13 @@ Authorization: Bearer <access_token>
|
||||
| PUT | `/clothes/:uuid` | 更新衣物属性 |
|
||||
| DELETE | `/clothes/:uuid` | 删除衣物(满足条件时回退上传额度) |
|
||||
|
||||
**穿搭(6)**
|
||||
**穿搭(7)**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/outfit` | 创建穿搭 |
|
||||
| GET | `/outfit` | 我的穿搭列表 |
|
||||
| GET | `/outfit/community` | 社区穿搭列表(其他用户公开的穿搭) |
|
||||
| GET | `/outfit/:uuid` | 穿搭详情 |
|
||||
| PUT | `/outfit/:uuid` | 更新穿搭 |
|
||||
| DELETE | `/outfit/:uuid` | 删除穿搭 |
|
||||
@@ -315,7 +316,7 @@ Authorization: Bearer <access_token>
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/outfit-share` | 创建穿搭分享(不扣额度,需穿搭已公开) |
|
||||
| POST | `/outfit-share` | 创建穿搭分享(无需公开到社区;私密穿搭扣 1 次衣柜分享额度) |
|
||||
| GET | `/outfit-share` | 我的穿搭分享列表 |
|
||||
| GET | `/outfit-share/received` | 分享给我的穿搭分享 |
|
||||
| GET | `/outfit-share/:uuid` | 穿搭分享详情(属主 / 访客视角) |
|
||||
@@ -350,7 +351,7 @@ Authorization: Bearer <access_token>
|
||||
| POST | `/order/:orderNo/cancel` | 取消订单 |
|
||||
| POST | `/order/refund` | 申请退款 |
|
||||
|
||||
> **注意**:当前**没有"社区广场 / 发现页"列表接口**。公开穿搭只能通过 UUID 直接访问详情(`GET /outfit/:uuid`),或出现在"我的收藏"列表中。
|
||||
> **说明**:社区广场列表接口为 `GET /outfit/community`(分页返回其他用户已公开且审核通过的穿搭,含作者信息与点赞/收藏计数)。
|
||||
|
||||
---
|
||||
|
||||
@@ -794,9 +795,9 @@ Authorization: Bearer <access_token>
|
||||
**业务逻辑(可见性)**
|
||||
|
||||
- 属主:始终可见。
|
||||
- 非属主:**仅当携带有效 `share_uuid`**(该分享正常、穿搭仍公开可见、且该衣物属于该分享的穿搭明细)时返回**只读**衣物信息;否则 `50005`。
|
||||
- 非属主:**仅当携带有效 `share_uuid`**(该分享正常、穿搭未被删除 / 封禁、且该衣物属于该分享的穿搭明细)时返回**只读**衣物信息;否则 `50005`。
|
||||
|
||||
**错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物 · `90103` 穿搭分享不存在(含穿搭已删除 / 取消公开)· `90104` 穿搭分享已取消
|
||||
**错误码**:`10000` 参数错误(uuid 为空)· `50000` 衣物不存在 · `50005` 无权操作该衣物 · `90103` 穿搭分享不存在(含穿搭已删除 / 已封禁)· `90104` 穿搭分享已取消
|
||||
|
||||
---
|
||||
|
||||
@@ -932,6 +933,49 @@ Authorization: Bearer <access_token>
|
||||
|
||||
---
|
||||
|
||||
#### GET `/outfit/community` — 社区穿搭列表(鉴权)
|
||||
|
||||
**业务逻辑**
|
||||
|
||||
- 返回**其他用户**已公开的穿搭:`is_public=2` + `audit_status=2` + `status=1`(与详情页他人可见性口径一致),并**排除本人**的穿搭(自己的公开穿搭在"我的穿搭"中查看)。
|
||||
- 列表按发布时间倒序(`id DESC`)。
|
||||
- 每项聚合作者信息与互动计数:`owner_name` / `owner_avatar`(无头像为空串)、`like_count` / `collect_count`(仅统计有效点赞/收藏)。
|
||||
|
||||
**请求**(query)
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| page / page_size | int | 否 | 分页,page_size 上限 50 |
|
||||
|
||||
**响应** `data`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| total | int64 | 总条数 |
|
||||
| page | int | 当前页 |
|
||||
| page_size | int | 每页条数 |
|
||||
| list | array | 社区穿搭数组,见下表 |
|
||||
|
||||
`list[]`:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| outfit_uuid | string | 穿搭 UUID |
|
||||
| outfit_name | string | 穿搭名称 |
|
||||
| outfit_desc | string | 穿搭描述 |
|
||||
| scene | string | 适合场景 |
|
||||
| source | int | `1`=用户创建 `2`=AI 推荐 |
|
||||
| created_at | int64 | 发布时间(秒级时间戳) |
|
||||
| items | array | 衣物明细 `[{clothes_uuid, processed_image}]`,`processed_image` 为可访问 URL |
|
||||
| owner_name | string | 作者昵称 |
|
||||
| owner_avatar | string | 作者头像 URL(无则空串) |
|
||||
| like_count | int64 | 点赞数 |
|
||||
| collect_count | int64 | 收藏数 |
|
||||
|
||||
**错误码**:`10000` 参数错误 · `19001` 数据库错误
|
||||
|
||||
---
|
||||
|
||||
#### GET `/outfit/:uuid` — 穿搭详情(鉴权)
|
||||
|
||||
**请求**(path)
|
||||
@@ -944,6 +988,7 @@ Authorization: Bearer <access_token>
|
||||
|
||||
- 本人穿搭:始终可见。
|
||||
- 他人穿搭:必须同时满足 `is_public=2` + `audit_status=2` + `status=1`,否则返回 `40004`。
|
||||
- 通过好友分享链接查看穿搭请使用 `GET /outfit-share/:uuid`(不要求穿搭已公开)。
|
||||
|
||||
**响应** `data` = [穿搭对象](#43-穿搭对象-outfititem)
|
||||
|
||||
@@ -1236,15 +1281,20 @@ Authorization: Bearer <access_token>
|
||||
|
||||
**状态枚举** `status`:`1`=正常 `2`=用户取消
|
||||
|
||||
> 穿搭分享用于把「自己的公开穿搭」以链接形式分享给好友。与衣柜分享不同,**创建分享不消耗任何额度**,但**要求穿搭已公开可见**(`is_public=2` 且 `audit_status=2` 且 `status=1`);一旦穿搭取消公开或被删除,相关分享即视为失效。
|
||||
> 穿搭分享用于把「自己的穿搭」以链接形式分享给好友,**与「公开到社区」完全解耦**:仅自己可见的穿搭同样可以分享,好友凭 `share_uuid` 只读查看整套穿搭与单品图。
|
||||
>
|
||||
> **额度**:分享**仅自己可见**的穿搭扣 1 次**衣柜分享额度**(`quota_type=4`,与衣柜分享共用同一个额度池);分享**已公开**的穿搭**不扣**(公开到社区时已扣过社区分享额度)。额度不足 → `80000` 且不创建分享。
|
||||
>
|
||||
> 分享一旦发出即独立于穿搭的公开状态——取消公开不影响已发出的分享,仅「取消分享」「删除穿搭」「封禁穿搭」才使分享失效。
|
||||
|
||||
#### POST `/outfit-share` — 创建穿搭分享(鉴权)
|
||||
|
||||
**业务逻辑**
|
||||
|
||||
1. 仅能分享**本人**的穿搭(他人 → `40004`)。
|
||||
2. 穿搭必须已公开可见,否则 `10000` + `details:["请先公开该穿搭再分享"]`。
|
||||
3. **不扣额度**;创建成功后返回 `share_uuid`,分享链接携带该值。
|
||||
2. **无需先公开到社区**:仅自己可见的穿搭同样可分享(分享与公开解耦);穿搭已被删除 → `40000`,已被封禁 → `40004`。
|
||||
3. **额度**:`is_public=1`(仅自己可见)→ 扣 1 次衣柜分享额度(`quota_type=4`,与衣柜分享共用池),额度不足 → `80000` 且**不创建分享**;`is_public=2`(已公开)→ **不扣**。扣额成功后分享落库失败会**自动回退** 1 次额度。
|
||||
4. 创建成功后返回 `share_uuid`,分享链接携带该值。
|
||||
|
||||
**请求**(json)
|
||||
|
||||
@@ -1258,7 +1308,7 @@ Authorization: Bearer <access_token>
|
||||
| --- | --- | --- |
|
||||
| share_uuid | string | 分享 UUID,供他人访问分享详情 |
|
||||
|
||||
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40004` 无权操作该穿搭 · `19001` 数据库错误
|
||||
**错误码**:`10000` 参数错误 · `40000` 穿搭不存在 · `40004` 无权操作该穿搭 · `80000` 额度不足 · `19001` 数据库错误
|
||||
|
||||
---
|
||||
|
||||
@@ -1295,7 +1345,7 @@ Authorization: Bearer <access_token>
|
||||
**业务逻辑**
|
||||
|
||||
- "访问即视为收到":登录用户(非分享属主)成功访问他人穿搭分享详情时,自动记录接收关系;重复访问不重复记录,属主访问不记录。
|
||||
- 列表按接收记录倒序分页返回,支持按 `status` 筛选。
|
||||
- 列表按接收记录倒序分页返回,支持按 `status` 筛选;**仅排除已删除 / 已封禁的穿搭**(穿搭是否公开不影响,分享与公开解耦)。
|
||||
- 字段同「我的穿搭分享列表」,并额外返回 `owner_name` / `owner_avatar`。
|
||||
|
||||
**错误码**:`10000` 参数错误 · `19001` 数据库错误
|
||||
@@ -1308,7 +1358,7 @@ Authorization: Bearer <access_token>
|
||||
|
||||
- 属主与访客都可访问(需已登录,`is_owner` 区分视角)。
|
||||
- **访客访问成功即记录接收关系**;属主访问不记录。
|
||||
- 分享已取消 → `90104`;分享不存在或对应穿搭已删除 / 取消公开 → `90103`。
|
||||
- 分享已取消 → `90104`;分享不存在或对应穿搭已删除 / 已封禁 → `90103`(穿搭取消公开**不影响**已发出的分享)。
|
||||
|
||||
**请求**(path)
|
||||
|
||||
@@ -1327,7 +1377,7 @@ Authorization: Bearer <access_token>
|
||||
| is_owner | bool | 是否本人分享 |
|
||||
| outfit | object | [穿搭对象](#43-穿搭对象-outfititem),`items[]` 含 `processed_image`(可直接展示) |
|
||||
|
||||
**错误码**:`10000` 参数错误 · `90103` 穿搭分享不存在(含穿搭已删除 / 取消公开)· `90104` 穿搭分享已取消
|
||||
**错误码**:`10000` 参数错误 · `90103` 穿搭分享不存在(含穿搭已删除 / 已封禁)· `90104` 穿搭分享已取消
|
||||
|
||||
---
|
||||
|
||||
@@ -1869,6 +1919,9 @@ stateDiagram-v2
|
||||
| **取消公开后再次公开** | **再扣 1 个额度** | ⚠️ 重点提示用户 |
|
||||
| 穿搭取消公开 | **不返还** | — |
|
||||
| 穿搭删除 | **不返还** | — |
|
||||
| 分享「仅自己可见」的穿搭给好友 | **扣 1 个衣柜分享额度**(与衣柜分享共用池) | 额度不足时引导购买 |
|
||||
| 分享「已公开」的穿搭给好友 | **不扣**(公开到社区时已扣社区分享额度) | — |
|
||||
| 私密穿搭分享但分享记录落库失败 | **自动回退** | 重试即可 |
|
||||
| AI 推荐生成(成功) | **扣 1 个 AI 额度** | 生成前展示剩余次数 |
|
||||
| AI 推荐失败(衣橱为空/模型失败/无有效单品/落库失败) | **自动回退** | 可安全重试 |
|
||||
| **AI 推荐成功但未保存** | **不返还** | ⚠️ 重点提示用户及时保存 |
|
||||
@@ -1878,7 +1931,7 @@ stateDiagram-v2
|
||||
| 购买商品支付成功 | **发放**(按订单快照) | 展示"已到账" |
|
||||
| 退款成功 | **回收**(`total >= used`,已用部分不退) | 提示额度已回收 |
|
||||
|
||||
**额度不足时**:前端跳商品列表引导购买。注意 AI 额度不足返回的是 `90401`(不是 `80000`)。
|
||||
**额度不足时**:前端跳商品列表引导购买。注意 AI 额度不足返回的是 `90401`(不是 `80000`)。私密穿搭分享额度不足返回 `80000`,此时**不要**回退为普通穿搭详情链接(私密穿搭对他人不可见,对方打开会是无权限的空页),应提示用户补充衣柜分享额度。
|
||||
|
||||
---
|
||||
|
||||
@@ -2220,8 +2273,8 @@ A:过期是**惰性**的。前端需同时判断 `status` 与 `expire_time`。
|
||||
**Q:`GET /outfit` 里混进了 AI 推荐的穿搭?**
|
||||
A:列表**不过滤 `status=3`(待用户保存)**。前端用 `source` 区分来源,AI 未保存的单独入口展示。
|
||||
|
||||
**Q:没有"社区广场"列表接口怎么办?**
|
||||
A:当前确实没有。公开穿搭只能通过 UUID 直接访问详情,或在"我的收藏"中出现。如需广场列表,需后端新增接口。
|
||||
**Q:如何浏览"社区广场"的公开穿搭?**
|
||||
A:调用 `GET /outfit/community` 分页获取其他用户已公开(公开 + 审核通过 + 正常)的穿搭列表,含作者信息与点赞/收藏计数;点击卡片进入 `GET /outfit/:uuid` 详情(他人视角)互动。
|
||||
|
||||
### 9.2 联调检查清单
|
||||
|
||||
|
||||
Reference in New Issue
Block a user