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.
 
 
 
 
 
 

339 lines
12 KiB

package console
import (
"strings"
"time"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
// ============================ 渠道商域 (channel) ============================
//
// 渠道商挂在品牌商下:一个品牌商可绑定多个渠道商,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%)。
// 语义为「从品牌商那份里再切」,故本身不会超过 100%。
const channelShareRateMax int32 = 10000
// canAuditChannel 是否有渠道商审批权(超管/管理员)。
func canAuditChannel(c *gin.Context) bool {
idt := currentIdentity(c)
return idt == pb.Identity_Admin || idt == pb.Identity_Manager
}
// getChannels 列出渠道商。前端传 brandid 过滤;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, effectiveBrandId(c, req.Brandid), 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})
}
// addOfficialChannel 给刚建好的品牌商配一条「官方渠道」:品牌自营的那条销售线。
// 与手工新增的渠道商不同——它随品牌商自动生成、直接启用(无需审批)、不配渠道账号
// (品牌自己的账号就管着它),分成比例固定 0:品牌那份提成本来就全归品牌自己,
// 再从自家渠道切一刀没有意义。name 留空则用「<品牌名>官方渠道」。
func addOfficialChannel(sys *appConn, brand *pb.DBBrand, name, operator string) (*pb.DBChannel, error) {
now := time.Now().Unix()
if name = strings.TrimSpace(name); name == "" {
name = brand.Brand + "官方渠道"
}
m := &pb.DBChannel{
Id: genChannelId(sys),
Brandid: brand.Id,
Name: name,
Contactmails: brand.Contactmails,
Remark: "品牌官方渠道(随品牌商自动创建,代表品牌自营销售)",
Sharerate: 0,
Status: channelStatusEnabled,
Official: true,
Auditaccount: operator,
Audittime: now,
Createtime: now,
Updatetime: now,
}
if err := dvAddChannel(sys, m); err != nil {
return nil, err
}
return m, nil
}
// 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
}
if m.Brandid == 0 {
// 品牌商账号不必选品牌商:默认就是自己。
m.Brandid = scopeOf(c).brandId
}
if m.Brandid == 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "所属品牌商必选")
return
}
// 品牌商账号只能在自己名下建渠道商;渠道商账号无权新建。
if currentIdentity(c) == pb.Identity_Dealer {
writeErr(c, pb.ErrorCode_InsufficientPermissions, "渠道商账号无权新增渠道商")
return
}
if !scopeOf(c).allowBrand(m.Brandid) {
writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权在该品牌商下新增渠道商")
return
}
if _, err := dvBrand(sys, m.Brandid); err != nil {
writeErr(c, pb.ErrorCode_ReqParameterError, "品牌商不存在")
return
}
if m.Sharerate < 0 || m.Sharerate > channelShareRateMax {
writeErr(c, pb.ErrorCode_ReqParameterError, "分成比例需在 0~10000(万分比) 之间")
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
}
}
}
now := time.Now().Unix()
m.Id = genChannelId(sys)
m.Official = false // 官方渠道只能由 addOfficialChannel 随品牌商生成,前端传什么都不作数
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 {
// 渠道商账号须同时绑品牌商与渠道商(validChannelBinding 的约定),渠道刚落库,绑定必然自洽。
acc, err := this.openBoundAccount(pb.Identity_Dealer, "Channel-"+m.Id,
req.AccountUsername, req.AccountPassword, req.AccountRemark,
m.Brandid, 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/brandid 不可改(改归属等于换了一个渠道商,
// 已生产的授权码会与新归属矛盾);状态只能走审批接口,避免绕过审批自行启用。
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 req.Channel.Sharerate < 0 || req.Channel.Sharerate > channelShareRateMax {
writeErr(c, pb.ErrorCode_ReqParameterError, "分成比例需在 0~10000(万分比) 之间")
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
// 分成比例只有有审批权的账号能改(品牌商自己改比例等于自定分成)。
if canAuditChannel(c) {
old.Sharerate = req.Channel.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
}
// 官方渠道是品牌商的一部分(品牌自营销售线),随品牌商生老病死,不单独删。
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 生产授权码时解析并校验渠道商:必选、须归属该品牌商、须为启用状态。
// 返回校验通过的渠道商;校验失败时已写好错误响应,调用方直接 return。
func resolveProductionChannelModel(c *gin.Context, sys *appConn, channelid string, brandid uint32) (*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.Brandid != brandid {
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 {
s := scopeOf(c)
return s.allowBrand(ch.Brandid) && s.allowChannel(ch.Id)
}