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