You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

287 lines
11 KiB

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)
}