diff --git a/app/main/api/desc/admin/admin_query.api b/app/main/api/desc/admin/admin_query.api index 1d3bb41..904104f 100644 --- a/app/main/api/desc/admin/admin_query.api +++ b/app/main/api/desc/admin/admin_query.api @@ -28,6 +28,10 @@ service main { @handler AdminGetQueryCleanupConfigList get /cleanup/configs (AdminGetQueryCleanupConfigListReq) returns (AdminGetQueryCleanupConfigListResp) + @doc "按 QueryId+FeatureApiId 补删单条查询的某个模块数据(仅内部运维使用)" + @handler AdminDeleteQueryFeatureData + post /cleanup/delete/feature (AdminDeleteQueryFeatureDataReq) returns (AdminDeleteQueryFeatureDataResp) + @doc "更新清理配置" @handler AdminUpdateQueryCleanupConfig put /cleanup/config (AdminUpdateQueryCleanupConfigReq) returns (AdminUpdateQueryCleanupConfigResp) @@ -131,3 +135,14 @@ type AdminGetQueryCleanupConfigListResp { type AdminUpdateQueryCleanupConfigResp { Success bool `json:"success"` // 是否成功 } + +// 运维补删接口:按 QueryId+FeatureApiId 删除单条查询中的指定模块数据 +type AdminDeleteQueryFeatureDataReq { + QueryId string `json:"query_id"` // 查询ID(Query表ID) + FeatureApiId string `json:"feature_api_id"` // 模块API标识(与前台 OfflineFeature 一致) +} + +type AdminDeleteQueryFeatureDataResp { + Success bool `json:"success"` // 是否删除成功(或数据本就不存在) + Message string `json:"message"` // 结果说明 +} diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api index 68e8a16..f2649c5 100644 --- a/app/main/api/desc/front/agent.api +++ b/app/main/api/desc/front/agent.api @@ -288,7 +288,7 @@ service main { @handler CheckFeatureWhitelistStatus get /whitelist/check (CheckFeatureWhitelistStatusReq) returns (CheckFeatureWhitelistStatusResp) - // 下架单个模块(创建订单并支付) + // 下架单个模块(统一入口:创建/补充白名单并根据 QueryId 删除本次报告中的模块数据) @handler OfflineFeature post /whitelist/offline (OfflineFeatureReq) returns (OfflineFeatureResp) diff --git a/app/main/api/internal/handler/admin_query/admindeletequeryfeaturedatahandler.go b/app/main/api/internal/handler/admin_query/admindeletequeryfeaturedatahandler.go new file mode 100644 index 0000000..b5dd590 --- /dev/null +++ b/app/main/api/internal/handler/admin_query/admindeletequeryfeaturedatahandler.go @@ -0,0 +1,29 @@ +package admin_query + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "ycc-server/app/main/api/internal/logic/admin_query" + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/common/result" + "ycc-server/pkg/lzkit/validator" +) + +func AdminDeleteQueryFeatureDataHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeleteQueryFeatureDataReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query.NewAdminDeleteQueryFeatureDataLogic(r.Context(), svcCtx) + resp, err := l.AdminDeleteQueryFeatureData(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index a717918..3301740 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -495,6 +495,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/cleanup/configs", Handler: admin_query.AdminGetQueryCleanupConfigListHandler(serverCtx), }, + { + // 按 QueryId+FeatureApiId 补删单条查询的某个模块数据(仅内部运维使用) + Method: http.MethodPost, + Path: "/cleanup/delete/feature", + Handler: admin_query.AdminDeleteQueryFeatureDataHandler(serverCtx), + }, { // 获取清理详情列表 Method: http.MethodGet, diff --git a/app/main/api/internal/logic/admin_query/admindeletequeryfeaturedatalogic.go b/app/main/api/internal/logic/admin_query/admindeletequeryfeaturedatalogic.go new file mode 100644 index 0000000..32d0001 --- /dev/null +++ b/app/main/api/internal/logic/admin_query/admindeletequeryfeaturedatalogic.go @@ -0,0 +1,51 @@ +package admin_query + +import ( + "context" + + "ycc-server/app/main/api/internal/svc" + "ycc-server/app/main/api/internal/types" + "ycc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminDeleteQueryFeatureDataLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeleteQueryFeatureDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeleteQueryFeatureDataLogic { + return &AdminDeleteQueryFeatureDataLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeleteQueryFeatureDataLogic) AdminDeleteQueryFeatureData(req *types.AdminDeleteQueryFeatureDataReq) (resp *types.AdminDeleteQueryFeatureDataResp, err error) { + // 基本参数校验 + if req.QueryId == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("QueryId 不能为空"), "") + } + if req.FeatureApiId == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("feature_api_id 不能为空"), "") + } + + // 使用事务调用 WhitelistService.DeleteFeatureFromQueryData,保持与其它删除逻辑一致 + err = l.svcCtx.QueryModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + return l.svcCtx.WhitelistService.DeleteFeatureFromQueryData(ctx, session, req.QueryId, req.FeatureApiId) + }) + if err != nil { + l.Errorf("Admin 删除查询模块数据失败, queryId=%s, featureApiId=%s, err=%+v", req.QueryId, req.FeatureApiId, err) + return nil, errors.Wrapf(xerr.NewErrMsg("删除失败,请稍后重试"), "") + } + + return &types.AdminDeleteQueryFeatureDataResp{ + Success: true, + Message: "删除成功(如果原本不存在该模块数据,则视为已删除)", + }, nil +} diff --git a/app/main/api/internal/logic/agent/createwhitelistorderlogic.go b/app/main/api/internal/logic/agent/createwhitelistorderlogic.go index 508b785..e533887 100644 --- a/app/main/api/internal/logic/agent/createwhitelistorderlogic.go +++ b/app/main/api/internal/logic/agent/createwhitelistorderlogic.go @@ -6,6 +6,7 @@ import ( "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" + "ycc-server/pkg/lzkit/lzUtils" "github.com/google/uuid" "github.com/pkg/errors" @@ -123,6 +124,7 @@ func (l *CreateWhitelistOrderLogic) CreateWhitelistOrder(req *types.CreateWhitel Id: uuid.NewString(), OrderNo: orderNo, UserId: userID, + OrderId: lzUtils.StringToNullString(req.OrderId), // 关联的查询订单ID(可选,用于后续支付回调精确删除报告) IdCard: req.IdCard, TotalAmount: totalAmount, Status: 1, // 待支付 diff --git a/app/main/api/internal/service/whitelistService.go b/app/main/api/internal/service/whitelistService.go index 8be8e2d..08c9a09 100644 --- a/app/main/api/internal/service/whitelistService.go +++ b/app/main/api/internal/service/whitelistService.go @@ -71,6 +71,8 @@ func (s *WhitelistService) EnsureFreeWhitelist( } } + // 约定:OrderId 字段在白名单表中用于记录“触发本次白名单的查询记录ID(QueryId)” + // 这里的 orderId 即为 OfflineFeature 请求中传入的 QueryId wl := &model.UserFeatureWhitelist{ Id: uuid.NewString(), IdCard: idCard, @@ -198,6 +200,8 @@ func (s *WhitelistService) ProcessOfflineFeature( } // 3. 检查是否已有白名单 + // 约定:只要调用 OfflineFeature,下架成功(包含“之前已存在白名单”的情况)都应删除“当前这一次查询”的报告数据 + // 因此,对于“已存在生效白名单”的场景,这里依然返回 whitelistCreated = true,触发上层 OfflineFeatureLogic 删除本次 QueryId 的模块数据 exists, err := s.CheckWhitelistExists(ctx, idCard, feature.Id) if err != nil { return false, 0, false, err @@ -297,6 +301,8 @@ func (s *WhitelistService) createWhitelistFromPaidOrder( paidOrderId string, price float64, ) error { + // 约定:OrderId 字段在白名单表中用于记录“触发本次白名单的查询记录ID(QueryId)” + // 这里的 orderId 即为 OfflineFeature 请求中传入的 QueryId wl := &model.UserFeatureWhitelist{ Id: uuid.NewString(), IdCard: idCard, @@ -517,7 +523,7 @@ func (s *WhitelistService) CheckQueryDataContainsFeature( return false, nil } -// ProcessPaidWhitelistOrder 处理已支付的白名单订单:创建白名单记录并删除报告数据 +// ProcessPaidWhitelistOrder 处理已支付的白名单订单:创建白名单记录,并在可精确定位时删除对应报告数据 // order: 支付订单(Order表) // whitelistOrder: 白名单订单(WhitelistOrder表) func (s *WhitelistService) ProcessPaidWhitelistOrder( @@ -539,8 +545,31 @@ func (s *WhitelistService) ProcessPaidWhitelistOrder( return errors.Wrap(err, "查询白名单订单明细失败") } - // 为每个明细创建白名单记录并删除报告数据 + // 为每个明细创建白名单记录,并在能够精确定位 Query 时删除对应报告数据 for _, item := range items { + var queryId string + + // 如果白名单订单绑定了查询订单ID(order_id),尝试通过 Query.order_id 精确找到对应的查询记录 + if whitelistOrder.OrderId.Valid && whitelistOrder.OrderId.String != "" { + queryModel, qErr := s.QueryModel.FindOneByOrderId(ctx, whitelistOrder.OrderId.String) + if qErr != nil { + if errors.Is(qErr, model.ErrNotFound) { + logx.Infof("白名单订单支付成功:订单 %s 绑定的查询订单 %s 未找到对应查询记录,跳过报告删除", whitelistOrder.OrderNo, whitelistOrder.OrderId.String) + } else { + return errors.Wrap(qErr, "根据白名单订单的查询订单ID查找查询记录失败") + } + } else { + queryId = queryModel.Id + // 精确删除该查询下当前模块的数据 + if delErr := s.DeleteFeatureFromQueryData(ctx, session, queryId, item.FeatureApiId); delErr != nil { + // 删除报告数据失败不影响白名单记录创建,只记录错误日志 + logx.Errorf("白名单订单支付后删除报告数据失败:查询记录 %s,模块 %s,错误:%v", queryId, item.FeatureApiId, delErr) + } else { + logx.Infof("白名单订单支付后删除报告数据成功:查询记录 %s,模块 %s", queryId, item.FeatureApiId) + } + } + } + // 创建白名单记录 wl := &model.UserFeatureWhitelist{ Id: uuid.NewString(), @@ -548,7 +577,9 @@ func (s *WhitelistService) ProcessPaidWhitelistOrder( FeatureId: item.FeatureId, FeatureApiId: item.FeatureApiId, UserId: whitelistOrder.UserId, - OrderId: lzUtils.StringToNullString(""), // 查询订单ID,如果有的话会在后续步骤中设置 + // 约定:UserFeatureWhitelist.OrderId 记录触发本次白名单的查询记录ID(QueryId) + // 对于支付回调场景,如果成功通过 order_id 找到 Query,则将 QueryId 回写到白名单记录中,便于后续审计 + OrderId: lzUtils.StringToNullString(queryId), WhitelistOrderId: lzUtils.StringToNullString(whitelistOrder.Id), Amount: item.Price, Status: 1, // 生效 @@ -556,13 +587,10 @@ func (s *WhitelistService) ProcessPaidWhitelistOrder( if _, err := s.UserFeatureWhitelistModel.Insert(ctx, session, wl); err != nil { return errors.Wrap(err, "创建白名单记录失败") } - - // 尝试删除报告数据 - // 注意:由于支付回调时可能不知道具体的查询订单ID,这里先尝试根据 id_card 查找 - // 如果找不到对应的报告,就跳过删除步骤(不影响主流程) - // 实际的报告数据删除应该在 OfflineFeature 接口中完成(如果提供了 orderId) - // 这里暂时不删除,因为无法确定是哪个具体的查询订单 - logx.Infof("白名单订单支付成功:订单 %s,模块 %s,已创建白名单记录。如需删除报告数据,请在 OfflineFeature 接口中提供查询订单ID", whitelistOrder.OrderNo, item.FeatureApiId) + // 如果无法通过 order_id 找到 Query,仅创建白名单记录,不删除任何报告数据,避免误删历史记录 + if !whitelistOrder.OrderId.Valid || whitelistOrder.OrderId.String == "" || queryId == "" { + logx.Infof("白名单订单支付成功:订单 %s,模块 %s,已创建白名单记录。由于缺少可用的查询订单ID或对应查询记录,未删除任何报告数据", whitelistOrder.OrderNo, item.FeatureApiId) + } } return nil diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go index 701cc5a..f62f93e 100644 --- a/app/main/api/internal/types/types.go +++ b/app/main/api/internal/types/types.go @@ -212,6 +212,16 @@ type AdminDeleteProductResp struct { Success bool `json:"success"` // 是否成功 } +type AdminDeleteQueryFeatureDataReq struct { + QueryId string `json:"query_id"` // 查询ID(Query表ID) + FeatureApiId string `json:"feature_api_id"` // 模块API标识(与前台 OfflineFeature 一致) +} + +type AdminDeleteQueryFeatureDataResp struct { + Success bool `json:"success"` // 是否删除成功(或数据本就不存在) + Message string `json:"message"` // 结果说明 +} + type AdminDeleteUserReq struct { Id string `path:"id"` // 用户ID } @@ -2052,8 +2062,8 @@ type ProductListItem struct { ProductEn string `json:"product_en"` // 英文名 Description string `json:"description"` // 描述 Notes string `json:"notes"` // 备注 - CostPrice float64 `json:"cost_price"` // 成本价(由功能成本累加得出) SellPrice float64 `json:"sell_price"` // 售价 + CostPrice float64 `json:"cost_price"` // 成本价(由功能成本累加得出) CreateTime string `json:"create_time"` // 创建时间 UpdateTime string `json:"update_time"` // 更新时间 } diff --git a/app/main/model/whitelistOrderModel_gen.go b/app/main/model/whitelistOrderModel_gen.go index 18137fa..c6eeb0f 100644 --- a/app/main/model/whitelistOrderModel_gen.go +++ b/app/main/model/whitelistOrderModel_gen.go @@ -66,6 +66,7 @@ type ( Version int64 `db:"version"` // 版本号(乐观锁) OrderNo string `db:"order_no"` // 订单号(唯一) UserId string `db:"user_id"` // 用户ID(代理的user_id) + OrderId sql.NullString `db:"order_id"` // 关联的查询订单ID(Query 表对应的订单ID,一般为 order.id) IdCard string `db:"id_card"` // 身份证号(查询对象标识) TotalAmount float64 `db:"total_amount"` // 总金额(单位:元) Status int64 `db:"status"` // 订单状态:1=待支付,2=已支付,3=已取消 @@ -89,11 +90,11 @@ func (m *defaultWhitelistOrderModel) Insert(ctx context.Context, session sqlx.Se yccWhitelistOrderIdKey := fmt.Sprintf("%s%v", cacheYccWhitelistOrderIdPrefix, data.Id) yccWhitelistOrderOrderNoKey := fmt.Sprintf("%s%v", cacheYccWhitelistOrderOrderNoPrefix, data.OrderNo) return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, whitelistOrderRowsExpectAutoSet) + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, whitelistOrderRowsExpectAutoSet) if session != nil { - return session.ExecCtx(ctx, query, data.Id, data.DeleteTime, data.DelState, data.Version, data.OrderNo, data.UserId, data.IdCard, data.TotalAmount, data.Status, data.PaymentMethod, data.PaymentPlatform, data.PlatformOrderId, data.PayTime) + return session.ExecCtx(ctx, query, data.Id, data.DeleteTime, data.DelState, data.Version, data.OrderNo, data.UserId, data.OrderId, data.IdCard, data.TotalAmount, data.Status, data.PaymentMethod, data.PaymentPlatform, data.PlatformOrderId, data.PayTime) } - return conn.ExecCtx(ctx, query, data.Id, data.DeleteTime, data.DelState, data.Version, data.OrderNo, data.UserId, data.IdCard, data.TotalAmount, data.Status, data.PaymentMethod, data.PaymentPlatform, data.PlatformOrderId, data.PayTime) + return conn.ExecCtx(ctx, query, data.Id, data.DeleteTime, data.DelState, data.Version, data.OrderNo, data.UserId, data.OrderId, data.IdCard, data.TotalAmount, data.Status, data.PaymentMethod, data.PaymentPlatform, data.PlatformOrderId, data.PayTime) }, yccWhitelistOrderIdKey, yccWhitelistOrderOrderNoKey) } func (m *defaultWhitelistOrderModel) insertUUID(data *WhitelistOrder) { @@ -160,9 +161,9 @@ func (m *defaultWhitelistOrderModel) Update(ctx context.Context, session sqlx.Se return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, whitelistOrderRowsWithPlaceHolder) if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id) + return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.OrderId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id) } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id) + return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.OrderId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id) }, yccWhitelistOrderIdKey, yccWhitelistOrderOrderNoKey) } @@ -183,9 +184,9 @@ func (m *defaultWhitelistOrderModel) UpdateWithVersion(ctx context.Context, sess sqlResult, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where `id` = ? and version = ? ", m.table, whitelistOrderRowsWithPlaceHolder) if session != nil { - return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id, oldVersion) + return session.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.OrderId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id, oldVersion) } - return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id, oldVersion) + return conn.ExecCtx(ctx, query, newData.DeleteTime, newData.DelState, newData.Version, newData.OrderNo, newData.UserId, newData.OrderId, newData.IdCard, newData.TotalAmount, newData.Status, newData.PaymentMethod, newData.PaymentPlatform, newData.PlatformOrderId, newData.PayTime, newData.Id, oldVersion) }, yccWhitelistOrderIdKey, yccWhitelistOrderOrderNoKey) if err != nil { return err diff --git a/deploy/script/gen_models.ps1 b/deploy/script/gen_models.ps1 index 22d6e76..d5f7270 100644 --- a/deploy/script/gen_models.ps1 +++ b/deploy/script/gen_models.ps1 @@ -58,11 +58,11 @@ $tables = @( # "query_cleanup_log", # "user", # "user_auth" - # "whitelist_order", + "whitelist_order" # "whitelist_order_item", # "user_feature_whitelist" # "complaint_main", - "complaint_alipay" + # "complaint_alipay" # "complaint_alipay_trade", # "complaint_manual" # "tianyuanapi_call_log"