package console import ( "strings" "time" "yunyan/pb" "github.com/gin-gonic/gin" ) // ============================ 渠道商域 (channel) ============================ // // 渠道商是全局实体(品牌商 2026-09-12 下线后不再有上级归属),id 为 8 位可读短码(主键)。 // 它是生产授权码、统计与月结算的共同维度——生产时选定的渠道商随批次落进每条授权码, // 用户绑定该设备后其后续数据即归属该渠道商。 // // 新增走审批:无审批权的账号添加的渠道商 status=0(待审核),须超管/管理员通过后 // (status=1 启用) 才能用于生产。超管/管理员自己新增的直接启用。 // // 数据在 console 主库(supabase),与产品/授权码同库,故复用设备域的 consoleDeviceConn()。 // 渠道商状态取值(与 DBChannel.status 及前端一致)。 const ( channelStatusPending int32 = 0 // 待审核 channelStatusEnabled int32 = 1 // 启用 channelStatusDisabled int32 = 2 // 停用 channelStatusRejected int32 = 3 // 驳回 ) // channelShareRateMax 渠道分成比例上限(万分比,10000 = 100%)。 // 现在校验的是【生产批次】上的比例(production_batch.sharerate),渠道商表里那列已停止维护。 const channelShareRateMax int32 = 10000 // canAuditChannel 是否有渠道商审批权(超管/管理员)。 func canAuditChannel(c *gin.Context) bool { idt := currentIdentity(c) return idt == pb.Identity_Admin || idt == pb.Identity_Manager } // getChannels 列出渠道商。status 不传(0) 与「待审核」冲突, // 故约定 status<0 为不限,前端默认传 -1。 func (this *serverComp) getChannels(c *gin.Context, sys *appConn) { var req pb.ApiGetChannelsReq _ = c.ShouldBindJSON(&req) // 渠道商账号:对结果按可见范围收敛。 models, err := dvChannels(sys, req.Status, req.Keyword) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiGetChannelsResp{Channels: scopeOf(c).filterChannels(models)}) } func (this *serverComp) getChannel(c *gin.Context, sys *appConn) { var req pb.ApiGetChannelReq _ = c.ShouldBindJSON(&req) if strings.TrimSpace(req.Id) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } model, err := dvChannel(sys, strings.TrimSpace(req.Id)) if err != nil { writeErr(c, pb.ErrorCode_DBError, "渠道商不存在: "+req.Id) return } if !channelVisible(c, model) { writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权访问该渠道商") return } writeOK(c, &pb.ApiGetChannelResp{Channel: model}) } // addChannel 新增渠道商。id 一律服务端生成短码,前端传入的 id 被忽略。 // 无审批权的账号提交后为待审核,且申请人记在 applyaccount 上。 // 勾选 create_account 时一并建一个绑定该渠道商的渠道商账号(identity=5,仅超管可建)。 func (this *serverComp) addChannel(c *gin.Context, sys *appConn) { var req pb.ApiAddChannelReq _ = c.ShouldBindJSON(&req) if req.Channel == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "channel 必填") return } m := req.Channel m.Name = strings.TrimSpace(m.Name) if m.Name == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "渠道商名称必填") return } // 渠道商是全局实体:品牌商 2026-09-12 整体下线后不再有上级归属,brandid 一律 0。 m.Brandid = 0 // 渠道商账号无权新建。 if currentIdentity(c) == pb.Identity_Dealer { writeErr(c, pb.ErrorCode_InsufficientPermissions, "渠道商账号无权新增渠道商") return } // 同时开渠道账号:超管/管理员。用户名/密码留空由服务端生成,显式填了才提前查重。 if req.CreateAccount { if !canCreateBoundAccount(c) { writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可在新增渠道商时同时创建渠道账号") return } if u := strings.TrimSpace(req.AccountUsername); u != "" { if msg := this.checkNewAccountName(u); msg != "" { writeErr(c, pb.ErrorCode_ReqParameterError, "渠道账号"+msg) return } } } // 分成比例已改为按生产批次设置(production_batch.sharerate),渠道商表单里不再有这一项。 // 新建一律 0,别从请求里读——留着会让「渠道商级比例」和「批次级比例」两个口径同时存在。 m.Sharerate = 0 now := time.Now().Unix() m.Id = genChannelId(sys) m.Official = false // 「官方渠道」原先随品牌商自动生成,品牌商下线后不再产出新的;存量行保持原值 m.Createtime = now m.Updatetime = now // 审批权决定落库状态:超管/管理员直接启用,其余一律待审核。 if canAuditChannel(c) { m.Status = channelStatusEnabled m.Auditaccount = ctxStr(c, "username") m.Audittime = now } else { m.Status = channelStatusPending m.Applyaccount = ctxStr(c, "username") m.Auditaccount = "" m.Audittime = 0 } m.Auditremark = "" if err := dvAddChannel(sys, m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } resp := &pb.ApiAddChannelResp{Channel: m} if req.CreateAccount { // 渠道商账号绑到刚落库的这个渠道商上,绑定必然自洽。 acc, err := this.openBoundAccount(pb.Identity_Dealer, "Channel-"+m.Id, req.AccountUsername, req.AccountPassword, req.AccountRemark, m.Id, "渠道商", m.Name, m.Contactmails) if err != nil { writeErr(c, pb.ErrorCode_DBError, "渠道商已创建,但渠道账号创建失败: "+err.Error()) return } resp.Account, resp.AccountPassword = acc.Username, acc.Password resp.MailTo, resp.MailError = acc.MailTo, acc.MailErr } writeOK(c, resp) } // updateChannel 更新渠道商资料。id 不可改(改它等于换了一个渠道商, // 已生产的授权码会与新归属矛盾);状态只能走审批接口,避免绕过审批自行启用。 func (this *serverComp) updateChannel(c *gin.Context, sys *appConn) { var req pb.ApiUpdateChannelReq _ = c.ShouldBindJSON(&req) if req.Channel == nil || strings.TrimSpace(req.Channel.Id) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "channel id 必填") return } old, err := dvChannel(sys, strings.TrimSpace(req.Channel.Id)) if err != nil { writeErr(c, pb.ErrorCode_DBError, "渠道商不存在: "+req.Channel.Id) return } if !channelVisible(c, old) { writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权修改该渠道商") return } if name := strings.TrimSpace(req.Channel.Name); name != "" { old.Name = name } old.Contact = req.Channel.Contact old.Contactmails = req.Channel.Contactmails old.Remark = req.Channel.Remark // Sharerate 刻意不写:比例已移到生产批次上,这里既不读也不清零—— // 清零会把存量渠道商已有的比例抹掉,而月结算目前还在读它。 old.Updatetime = time.Now().Unix() if err = dvSaveChannel(sys, old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiUpdateChannelResp{Channel: old}) } // delChannel 删除渠道商。已产生生产批次的渠道商禁止删除——历史授权码/统计/结算都按 id 归属, // 删掉会让这些数据指向不存在的渠道;要停用请走审批接口置 status=2。 func (this *serverComp) delChannel(c *gin.Context, sys *appConn) { var req pb.ApiDelChannelReq _ = c.ShouldBindJSON(&req) id := strings.TrimSpace(req.Id) if id == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } ch, err := dvChannel(sys, id) if err != nil { writeErr(c, pb.ErrorCode_DBError, "渠道商不存在: "+id) return } if !channelVisible(c, ch) { writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权删除该渠道商") return } // 存量「官方渠道」是品牌商时代随品牌自动建的。品牌商已下线,但这些渠道上挂着历史批次与结算, // 仍不允许删——要停用请走审批置 status=2。 if ch.Official { writeErr(c, pb.ErrorCode_ReqParameterError, "官方渠道不能删除(品牌商时代自动创建的历史渠道);如需停用请走审批") return } n, err := dvCountBatchesByChannel(sys, id) if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if n > 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "该渠道商已有生产批次,不能删除,请改为停用") return } if err := dvDelChannel(sys, id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiDelChannelResp{Id: id}) } // auditChannel 审批/启停渠道商:通过(1) / 停用(2) / 驳回(3)。仅超管、管理员。 func (this *serverComp) auditChannel(c *gin.Context, sys *appConn) { var req pb.ApiAuditChannelReq _ = c.ShouldBindJSON(&req) if !canAuditChannel(c) { writeErr(c, pb.ErrorCode_InsufficientPermissions, "仅超管/管理员可审批渠道商") return } id := strings.TrimSpace(req.Id) if id == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } switch req.Status { case channelStatusEnabled, channelStatusDisabled, channelStatusRejected: default: writeErr(c, pb.ErrorCode_ReqParameterError, "status 仅支持 1(通过启用) / 2(停用) / 3(驳回)") return } old, err := dvChannel(sys, id) if err != nil { writeErr(c, pb.ErrorCode_DBError, "渠道商不存在: "+id) return } old.Status = req.Status old.Auditaccount = ctxStr(c, "username") old.Auditremark = req.Auditremark old.Audittime = time.Now().Unix() old.Updatetime = old.Audittime if err = dvSaveChannel(sys, old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &pb.ApiAuditChannelResp{Channel: old}) } // resolveProductionChannelModel 生产 MAC/授权码时解析并校验渠道商:必选、须为启用状态。 // // 渠道商是全局实体,不再有品牌归属可校验(品牌商 2026-09-12 下线)。 // 返回校验通过的渠道商;校验失败时已写好错误响应,调用方直接 return。 func resolveProductionChannelModel(c *gin.Context, sys *appConn, channelid string) (*pb.DBChannel, bool) { id := strings.TrimSpace(channelid) if id == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "渠道商必选") return nil, false } ch, err := dvChannel(sys, id) if err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, "渠道商不存在: "+id) return nil, false } if ch.Status != channelStatusEnabled { writeErr(c, pb.ErrorCode_ReqParameterError, "渠道商未启用(待审核/停用/驳回),不能用于生产: "+id) return nil, false } return ch, true } // channelVisible 该渠道商是否在当前账号的可见范围内: // 渠道商账号只看自己那一个,超管/管理员不受限。 func channelVisible(c *gin.Context, ch *pb.DBChannel) bool { return scopeOf(c).allowChannel(ch.Id) }