Files
hfb_sys/backend/internal/modules/invitation/handler.go
T
yml2213 984433f866 feat(invitation): 实现邀请返利系统、专属注册落地页与分成管理
- 数据库与数据模型:新增 migration 000081,支持用户专属邀请码、被邀请人绑定与实名时间戳;新增邀请返利配置表与结算明细表
- 业务与结算逻辑:实现首月 60%、次月 30%、往后 15% 阶梯返利算法;买卖双向独立寻源并根据订单平台利润 50% 单边基准核算;与终态订单结算和钱包可用余额强幂等联动
- 分销站点支持:支持主站与加盟分站独立配置返利比例;跨站交易按照收号端与售卖端独立出资,资金互不侵蚀
- 前端交互与入口:
  - 新增专属短链注册落地页 /i/:code,自动带入并锁定邀请码,引导实名认证
  - 个人中心与移动端新增邀请返利看板(专属码、推广链接、统计面板、好友记录与返利流水明细)
  - 管理后台新增邀请返利管理(比例配置、邀请关系与返利流水审计)
- 验证方式:
  - 通过 repository_test.go 单元测试(阶梯区间、实名激活、跨站多方分流验证)
  - 通过 npm run format、npm run check 与 scripts/check.sh 全量门禁校验
2026-10-10 12:48:59 +08:00

228 lines
5.5 KiB
Go

package invitation
import (
"errors"
"net/http"
"strconv"
"strings"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/internal/sitecode"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
// GetInviteInfo 公开接口:根据邀请码查询邀请人简要信息(用于落地注册页)
func (h *Handler) GetInviteInfo(c *gin.Context) {
code := strings.TrimSpace(c.Param("code"))
if code == "" {
code = strings.TrimSpace(c.Query("code"))
}
info, err := h.service.GetInviteCodeInfo(c.Request.Context(), code)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, info)
}
// UserSummary 个人中心邀请返利概览
func (h *Handler) UserSummary(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
siteID := parseSiteID(c)
summary, err := h.service.GetUserSummary(c.Request.Context(), userID, siteID)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, summary)
}
// ListMyInvitedUsers 我的邀请好友列表
func (h *Handler) ListMyInvitedUsers(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
page, pageSize := parsePagination(c)
items, total, err := h.service.ListMyInvitedUsers(c.Request.Context(), userID, page, pageSize)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, gin.H{
"items": items,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// ListMyRebates 我的返利流水
func (h *Handler) ListMyRebates(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
page, pageSize := parsePagination(c)
items, total, err := h.service.ListMyRebates(c.Request.Context(), userID, page, pageSize)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, gin.H{
"items": items,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// AdminGetConfig 管理员获取返利比例配置
func (h *Handler) AdminGetConfig(c *gin.Context) {
siteID := parseAdminSiteID(c)
cfg, err := h.service.GetRebateConfig(c.Request.Context(), siteID)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, cfg)
}
// AdminUpdateConfig 管理员更新返利比例配置
func (h *Handler) AdminUpdateConfig(c *gin.Context) {
siteID := parseAdminSiteID(c)
var req UpdateConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "比例参数格式不正确,必须在0到1之间")
return
}
cfg, err := h.service.UpdateRebateConfig(c.Request.Context(), siteID, req)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, cfg)
}
// AdminInvitations 管理员查询全量邀请关系
func (h *Handler) AdminInvitations(c *gin.Context) {
var query AdminInvitationQuery
if err := c.ShouldBindQuery(&query); err != nil {
response.BadRequest(c, "查询参数不正确")
return
}
items, total, err := h.service.ListAdminInvitations(c.Request.Context(), query)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, gin.H{
"items": items,
"total": total,
"page": query.Page,
"page_size": query.PageSize,
})
}
// AdminRebates 管理员查询全量返利流水
func (h *Handler) AdminRebates(c *gin.Context) {
var query AdminRebateLedgerQuery
if err := c.ShouldBindQuery(&query); err != nil {
response.BadRequest(c, "查询参数不正确")
return
}
items, total, err := h.service.ListAdminRebateLedgers(c.Request.Context(), query)
if err != nil {
writeInvitationError(c, err)
return
}
response.OK(c, gin.H{
"items": items,
"total": total,
"page": query.Page,
"page_size": query.PageSize,
})
}
func currentUserID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextUserID)
if !ok {
return 0, false
}
userID, ok := value.(uint64)
return userID, ok
}
func parsePagination(c *gin.Context) (int, int) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
return page, pageSize
}
func parseSiteID(c *gin.Context) uint64 {
siteCode := sitecode.FromRequest(c)
if strings.TrimSpace(siteCode) == "" {
return 0
}
// 可从上下文或按 siteCode 获取 siteID
return 0
}
func parseAdminSiteID(c *gin.Context) uint64 {
raw := c.Query("distribution_site_id")
if raw == "" {
raw = c.Param("id")
}
if raw == "" {
raw = c.Param("site_id")
}
if raw != "" {
id, _ := strconv.ParseUint(raw, 10, 64)
return id
}
return 0
}
func writeInvitationError(c *gin.Context, err error) {
response.RecordError(c, err)
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库服务不可用")
case errors.Is(err, ErrInvalidInviteCode):
response.BadRequest(c, "邀请码不存在或无效")
case errors.Is(err, ErrSelfInviteNotAllowed):
response.BadRequest(c, "不能自己邀请自己")
case errors.Is(err, ErrAlreadyInvited):
response.BadRequest(c, "用户已绑定过邀请人")
case errors.Is(err, ErrInviterNotVerified):
response.BadRequest(c, "邀请人尚未完成实名认证")
default:
response.Error(c, http.StatusInternalServerError, "invitation_error", err.Error())
}
}