package service import ( "context" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "observer-server/biz/dao" "observer-server/biz/model/dto" "observer-server/common" ) // licenseService 授权业务:查询(缓存)、管理端手动授权/撤销(单写者串行事务) type licenseService struct{} var License = &licenseService{} // Get 查询授权:expires_at > now 视为 active(未充值 NULL 即未授权); // 手机号由登录中间件从 token 解出;查询走缓存(落授权后已清缓存)。 func (s *licenseService) Get(ctx context.Context, req *dto.LicenseReq) (*dto.LicenseRes, error) { lic, err := dao.License.GetByPhone(ctx, common.PhoneFromCtx(ctx)) if err != nil { return nil, err } if lic == nil || lic.ExpiresAt == nil || !lic.ExpiresAt.Time.After(time.Now()) { return &dto.LicenseRes{Active: false}, nil } return &dto.LicenseRes{Active: true, ExpiresAt: lic.ExpiresAt}, nil } // CalcExpiresAt 计算自然日到期时间:base 所在日 0 点 + days 天(day 套餐即当天 24:00 失效)。 // 续费叠加由调用方传入 base = max(现有到期, now),本方法不感知现有授权。 func (s *licenseService) CalcExpiresAt(base time.Time, days int) *gtime.Time { start := time.Date(base.Year(), base.Month(), base.Day(), 0, 0, 0, 0, base.Location()) return gtime.New(start.AddDate(0, 0, days)) } // AdminListLicenses 管理端账号/授权分页列表(含未充值账号) func (s *licenseService) AdminListLicenses(ctx context.Context, req *dto.AdminLicenseListReq) (*dto.AdminLicenseListRes, error) { page, size := common.NormalizePage(req.Page, req.Size) list, total, err := dao.License.PageByPhone(ctx, req.PhoneNum, page, size) if err != nil { return nil, err } items := make([]*dto.AdminLicenseItem, 0, len(list)) for _, lic := range list { items = append(items, &dto.AdminLicenseItem{ PhoneNum: lic.PhoneNum, ExpiresAt: lic.ExpiresAt, Remark: lic.Remark, CreatedAt: lic.CreatedAt, UpdatedAt: lic.UpdatedAt, }) } return &dto.AdminLicenseListRes{Total: total, List: items}, nil } // AdminGrant 手动授权(免费发卡):与支付回调 persistPaid 同语义——单写者串行事务内 // base = max(现有到期, now) 后按自然日叠加,已过期账号从当前时间起算; // 未注册账号自动建占位行(无密码,用户注册时补密码);提交后清授权缓存。 func (s *licenseService) AdminGrant(ctx context.Context, req *dto.AdminGrantReq) (*dto.AdminGrantRes, error) { plan, err := common.GetPlan(ctx, req.PlanId) if err != nil { return nil, err } if plan == nil { return nil, gerror.NewCode(common.CodePlanNotConfigured, "套餐不存在") } var expiresAt *gtime.Time err = common.Serial().Submit(ctx, func() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { lic, err := dao.License.GetByPhoneInTx(ctx, tx, req.PhoneNum) if err != nil { return err } base := time.Now() if lic != nil && lic.ExpiresAt != nil && lic.ExpiresAt.Time.After(base) { base = lic.ExpiresAt.Time } expiresAt = License.CalcExpiresAt(base, plan.Days) return dao.License.UpsertAuthInTx(ctx, tx, req.PhoneNum, expiresAt, gtime.Now()) }) }) if err != nil { return nil, err } common.ClearCache(ctx, "license:"+req.PhoneNum) return &dto.AdminGrantRes{ExpiresAt: expiresAt}, nil } // AdminRevoke 撤销授权:清空授权字段、保留账号行(密码不动),提交后清缓存。 func (s *licenseService) AdminRevoke(ctx context.Context, req *dto.AdminRevokeReq) (*dto.AdminRevokeRes, error) { err := common.Serial().Submit(ctx, func() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { return dao.License.ClearAuthInTx(ctx, tx, req.PhoneNum) }) }) if err != nil { return nil, err } common.ClearCache(ctx, "license:"+req.PhoneNum) return &dto.AdminRevokeRes{}, nil } // AdminRemark 写账号备注(管理端运营记录):空串即清空,不覆盖密码/授权; // 单写者串行事务内更新,提交后清授权缓存(列表不缓存不受影响)。 func (s *licenseService) AdminRemark(ctx context.Context, req *dto.AdminRemarkReq) (*dto.AdminRemarkRes, error) { err := common.Serial().Submit(ctx, func() error { return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { return dao.License.SetRemarkInTx(ctx, tx, req.PhoneNum, req.Remark) }) }) if err != nil { return nil, err } common.ClearCache(ctx, "license:"+req.PhoneNum) return &dto.AdminRemarkRes{}, nil }