tokenFactory/model/supplier_application.go

964 lines
35 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import (
"errors"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
// SupplierApplicationStatusPending 表示待审核。
SupplierApplicationStatusPending = 0
// SupplierApplicationStatusApproved 表示审核通过。
SupplierApplicationStatusApproved = 1
// SupplierApplicationStatusRejected 表示审核驳回。
SupplierApplicationStatusRejected = 2
// SupplierApplicationStatusDeactivated 表示供应商已注销。
SupplierApplicationStatusDeactivated = 3
)
const (
// SupplierApplicationAuditActionSubmit 表示提交申请。
SupplierApplicationAuditActionSubmit = 0
// SupplierApplicationAuditActionApprove 表示审核通过。
SupplierApplicationAuditActionApprove = 1
// SupplierApplicationAuditActionReject 表示审核驳回。
SupplierApplicationAuditActionReject = 2
// SupplierApplicationAuditActionDeactivate 表示供应商注销。
SupplierApplicationAuditActionDeactivate = 3
// SupplierApplicationAuditActionActivate 表示供应商重新启用。
SupplierApplicationAuditActionActivate = 4
)
const (
// UserMessageBizTypeSupplierApplication 供应商申请业务类型。
UserMessageBizTypeSupplierApplication = "supplier_application"
// UserMessageTypeSupplierSubmitted 供应商提交待审核消息。
UserMessageTypeSupplierSubmitted = "supplier_submitted"
// UserMessageTypeSupplierApproved 供应商审核通过消息。
UserMessageTypeSupplierApproved = "supplier_approved"
// UserMessageTypeSupplierRejected 供应商审核驳回消息。
UserMessageTypeSupplierRejected = "supplier_rejected"
)
var (
// ErrSupplierApplicationAlreadyReviewed 表示申请已被其他管理员处理。
ErrSupplierApplicationAlreadyReviewed = errors.New("supplier application already reviewed")
// ErrSupplierApplicationStatusNotEditable 表示申请当前状态不可修改(已审核通过)。
ErrSupplierApplicationStatusNotEditable = errors.New("supplier application status is not editable")
// ErrSupplierApplicationStatusNotApproved 表示申请当前不处于审核通过状态,不能执行供应商注销。
ErrSupplierApplicationStatusNotApproved = errors.New("supplier application status is not approved")
// ErrSupplierApplicationStatusNotDeactivated 表示申请当前不处于已注销状态,不能执行供应商启用。
ErrSupplierApplicationStatusNotDeactivated = errors.New("supplier application status is not deactivated")
)
// SupplierApplication 供应商入驻申请主表。
type SupplierApplication struct {
ID int `json:"id" gorm:"primaryKey;comment:主键ID"`
ApplicantUserID int `json:"applicant_user_id" gorm:"index;not null;comment:申请人用户ID"`
ApplicantUsername string `json:"applicant_username" gorm:"column:applicant_username;->;comment:申请人用户名(关联 users.username"`
ApplicantQuota int `json:"applicant_quota" gorm:"column:applicant_quota;->;comment:申请人账户剩余额度(列表联表回填)"`
ApplicantUsedQuota int `json:"applicant_used_quota" gorm:"column:applicant_used_quota;->;comment:申请人历史累计已用额度(列表联表回填)"`
CompanyName string `json:"company_name" gorm:"type:varchar(255);not null;comment:企业或主体名称"`
CreditCode string `json:"credit_code" gorm:"type:varchar(32);not null;uniqueIndex;comment:统一社会信用代码"`
BusinessLicenseURL string `json:"business_license_url" gorm:"type:varchar(1024);not null;comment:营业执照文件URL"`
BusinessLicenseFile string `json:"business_license_file" gorm:"type:varchar(255);not null;default:'';comment:营业执照文件名称"`
CompanyLogoURL string `json:"company_logo_url" gorm:"type:varchar(1024);not null;default:'';comment:企业Logo图片URL"`
SupplierType string `json:"supplier_type" gorm:"type:varchar(64);not null;default:'';comment:供应商类型"`
LegalRepresentative string `json:"legal_representative" gorm:"type:varchar(128);not null;comment:法人或经营者姓名"`
CompanySize string `json:"company_size" gorm:"type:varchar(64);comment:企业规模"`
ContactName string `json:"contact_name" gorm:"type:varchar(128);not null;comment:对接人姓名"`
ContactMobile string `json:"contact_mobile" gorm:"type:varchar(32);not null;comment:对接人手机号"`
ContactWechat string `json:"contact_wechat" gorm:"type:varchar(128);not null;comment:对接人微信或企业微信"`
SupplierAlias *string `json:"supplier_alias" gorm:"type:varchar(128);uniqueIndex;comment:供应商别名,创建时默认 P+主键 id可修改"`
SupplierCapability *SupplierCapability `json:"supplier_capability" gorm:"-"`
Status int `json:"status" gorm:"type:int;index;default:0;not null;comment:审核状态 0待审核 1已通过 2已驳回 3已注销"`
ReviewReason string `json:"review_reason" gorm:"type:text;comment:审核备注或驳回原因"`
ReviewedBy int `json:"reviewed_by" gorm:"type:int;index;default:0;comment:审核人用户ID"`
ReviewedAt int64 `json:"reviewed_at" gorm:"type:bigint;default:0;comment:审核时间戳"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;index;comment:创建时间戳"`
UpdatedAt int64 `json:"updated_at" gorm:"type:bigint;comment:更新时间戳"`
}
// SupplierApplicationAudit 供应商审核极简审计表。
type SupplierApplicationAudit struct {
ID int `json:"id" gorm:"primaryKey;comment:主键ID"`
ApplicationID int `json:"application_id" gorm:"index;not null;comment:供应商申请ID"`
OperatorUserID int `json:"operator_user_id" gorm:"index;not null;comment:操作人用户ID"`
Action int `json:"action" gorm:"type:int;index;not null;comment:操作类型 0提交 1通过 2驳回"`
FromStatus int `json:"from_status" gorm:"type:int;not null;comment:变更前状态"`
ToStatus int `json:"to_status" gorm:"type:int;not null;comment:变更后状态"`
Reason string `json:"reason" gorm:"type:text;comment:审核备注或驳回原因"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;index;comment:创建时间戳"`
}
// UserMessage 通用站内消息表(支持按用户与按角色广播)。
type UserMessage struct {
ID int `json:"id" gorm:"primaryKey;comment:主键ID"`
ReceiverUserID int `json:"receiver_user_id" gorm:"type:int;index;default:0;comment:接收用户ID 0表示广播"`
ReceiverMinRole int `json:"receiver_min_role" gorm:"type:int;index;default:0;comment:广播最小角色门槛"`
Type string `json:"type" gorm:"type:varchar(64);index;comment:消息类型"`
Title string `json:"title" gorm:"type:varchar(255);not null;comment:消息标题"`
Content string `json:"content" gorm:"type:text;not null;comment:消息内容"`
BizType string `json:"biz_type" gorm:"type:varchar(64);index;comment:业务类型"`
BizID int `json:"biz_id" gorm:"type:int;index;default:0;comment:业务ID"`
IsRead bool `json:"is_read" gorm:"type:boolean;index;default:false;comment:是否已读"`
ReadAt int64 `json:"read_at" gorm:"type:bigint;default:0;comment:已读时间戳"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;index;comment:创建时间戳"`
}
// UserMessageRead 站内广播消息的用户已读记录。
// 仅用于 receiver_user_id=0 的广播消息按用户追踪已读状态。
type UserMessageRead struct {
ID int `json:"id" gorm:"primaryKey;comment:主键ID"`
UserID int `json:"user_id" gorm:"type:int;not null;uniqueIndex:idx_user_message_reads_user_message,priority:1;comment:用户ID"`
MessageID int `json:"message_id" gorm:"type:int;not null;uniqueIndex:idx_user_message_reads_user_message,priority:2;comment:消息ID"`
ReadAt int64 `json:"read_at" gorm:"type:bigint;not null;default:0;comment:已读时间戳"`
CreatedAt int64 `json:"created_at" gorm:"type:bigint;index;comment:创建时间戳"`
}
// SupplierSimplePricingItem pricing 接口使用的供应商精简信息。
type SupplierSimplePricingItem struct {
SupplierID int `json:"supplier_id"`
SupplierName string `json:"supplier_name"`
}
// SupplierApplicationAutoAlias 供应商库内编号P + 主键 id创建/编辑后由服务端写入,无需前端传入)。
func SupplierApplicationAutoAlias(id int) string {
if id <= 0 {
return ""
}
return "P" + strconv.Itoa(id)
}
func ptrSupplierApplicationAlias(id int) *string {
if id <= 0 {
return nil
}
s := SupplierApplicationAutoAlias(id)
return &s
}
// CreateSupplierApplication 创建供应商申请记录。
func CreateSupplierApplication(app *SupplierApplication) error {
now := time.Now().Unix()
app.CreatedAt = now
app.UpdatedAt = now
if err := DB.Create(app).Error; err != nil {
return err
}
alias := SupplierApplicationAutoAlias(app.ID)
if err := DB.Model(app).Update("supplier_alias", alias).Error; err != nil {
return err
}
app.SupplierAlias = ptrSupplierApplicationAlias(app.ID)
return nil
}
// CreateSupplierApplicationAutoApproved 创建直接审核通过的供应商申请。
// 该方法用于管理员及以上角色提交申请时,保证申请创建、用户绑定与审计记录在同一事务内完成。
func CreateSupplierApplicationAutoApproved(app *SupplierApplication, reviewerUserID int) error {
tx := DB.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
now := time.Now().Unix()
app.Status = SupplierApplicationStatusApproved
app.ReviewedBy = reviewerUserID
app.ReviewedAt = now
app.ReviewReason = ""
app.CreatedAt = now
app.UpdatedAt = now
if err := tx.Create(app).Error; err != nil {
tx.Rollback()
return err
}
alias := SupplierApplicationAutoAlias(app.ID)
if err := tx.Model(&SupplierApplication{}).Where("id = ?", app.ID).Update("supplier_alias", alias).Error; err != nil {
tx.Rollback()
return err
}
app.SupplierAlias = ptrSupplierApplicationAlias(app.ID)
if err := tx.Model(&User{}).Where("id = ?", app.ApplicantUserID).
Updates(map[string]any{
"supplier_id": app.ID,
}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&SupplierApplicationAudit{
ApplicationID: app.ID,
OperatorUserID: app.ApplicantUserID,
Action: SupplierApplicationAuditActionSubmit,
FromStatus: SupplierApplicationStatusPending,
ToStatus: SupplierApplicationStatusPending,
Reason: "",
CreatedAt: now,
}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&SupplierApplicationAudit{
ApplicationID: app.ID,
OperatorUserID: reviewerUserID,
Action: SupplierApplicationAuditActionApprove,
FromStatus: SupplierApplicationStatusPending,
ToStatus: SupplierApplicationStatusApproved,
Reason: "管理员提交自动通过",
CreatedAt: now,
}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
// CreateSupplierApplicationAudit 创建供应商审核审计记录。
func CreateSupplierApplicationAudit(audit *SupplierApplicationAudit) error {
audit.CreatedAt = time.Now().Unix()
return DB.Create(audit).Error
}
// ListSupplierApplications 分页查询供应商申请(管理员)。
func ListSupplierApplications(status *int, pageInfo *common.PageInfo) ([]*SupplierApplication, int64, error) {
var (
items []*SupplierApplication
total int64
)
query := DB.Model(&SupplierApplication{})
if status != nil {
query = query.Where("status = ?", *status)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("id desc").
Limit(pageInfo.GetPageSize()).
Offset(pageInfo.GetStartIdx()).
Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
// ListSupplierApplicationsByApplicant 分页查询当前用户提交的申请。
func ListSupplierApplicationsByApplicant(applicantUserID int, status *int, pageInfo *common.PageInfo) ([]*SupplierApplication, int64, error) {
var (
items []*SupplierApplication
total int64
)
query := DB.Model(&SupplierApplication{}).Where("applicant_user_id = ?", applicantUserID)
if status != nil {
query = query.Where("status = ?", *status)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("id desc").
Limit(pageInfo.GetPageSize()).
Offset(pageInfo.GetStartIdx()).
Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
// ListSuppliersByCompanyName 分页查询供应商列表(支持按供应商名称模糊搜索与状态筛选)。
func ListSuppliersByCompanyName(companyName string, statuses []int, pageInfo *common.PageInfo) ([]*SupplierApplication, int64, error) {
var (
items []*SupplierApplication
total int64
)
query := DB.Model(&SupplierApplication{}).
Joins("LEFT JOIN users ON users.id = supplier_applications.applicant_user_id")
if len(statuses) > 0 {
query = query.Where("supplier_applications.status IN ?", statuses)
}
if strings.TrimSpace(companyName) != "" {
query = query.Where("company_name LIKE ?", "%"+strings.TrimSpace(companyName)+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.
Select("supplier_applications.*, users.username AS applicant_username, COALESCE(users.quota,0) AS applicant_quota, COALESCE(users.used_quota,0) AS applicant_used_quota").
Order("supplier_applications.id desc").
Limit(pageInfo.GetPageSize()).
Offset(pageInfo.GetStartIdx()).
Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
// GetSupplierByID 根据供应商申请ID查询供应商详情含申请人用户名
func GetSupplierByID(supplierID int) (*SupplierApplication, error) {
var item SupplierApplication
err := DB.Model(&SupplierApplication{}).
Select("supplier_applications.*, users.username AS applicant_username, COALESCE(users.quota,0) AS applicant_quota, COALESCE(users.used_quota,0) AS applicant_used_quota").
Joins("LEFT JOIN users ON users.id = supplier_applications.applicant_user_id").
Where("supplier_applications.id = ?", supplierID).
First(&item).Error
if err != nil {
return nil, err
}
return &item, nil
}
// GetMySupplierApplication 获取当前用户最近一条供应商申请按ID倒序
func GetMySupplierApplication(applicantUserID int) (*SupplierApplication, error) {
var app SupplierApplication
err := DB.Where("applicant_user_id = ?", applicantUserID).
Order("id desc").
First(&app).Error
if err != nil {
return nil, err
}
return &app, nil
}
// UpdateMySupplierApplication 修改当前用户指定ID申请仅已通过状态不可修改
func UpdateMySupplierApplication(applicantUserID int, applicationID int, req *SupplierApplication) (*SupplierApplication, error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var app SupplierApplication
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("applicant_user_id = ? AND id = ?", applicantUserID, applicationID).
First(&app).Error; err != nil {
tx.Rollback()
return nil, err
}
if app.Status == SupplierApplicationStatusApproved {
tx.Rollback()
return nil, ErrSupplierApplicationStatusNotEditable
}
fromStatus := app.Status
now := time.Now().Unix()
updates := map[string]any{
"company_name": req.CompanyName,
"credit_code": req.CreditCode,
"business_license_url": req.BusinessLicenseURL,
"business_license_file": req.BusinessLicenseFile,
"company_logo_url": req.CompanyLogoURL,
"legal_representative": req.LegalRepresentative,
"company_size": req.CompanySize,
"contact_name": req.ContactName,
"contact_mobile": req.ContactMobile,
"contact_wechat": req.ContactWechat,
"status": SupplierApplicationStatusPending,
"review_reason": "",
"reviewed_by": 0,
"reviewed_at": 0,
"updated_at": now,
}
if err := tx.Model(&SupplierApplication{}).
Where("id = ?", app.ID).
Updates(updates).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Create(&SupplierApplicationAudit{
ApplicationID: app.ID,
OperatorUserID: applicantUserID,
Action: SupplierApplicationAuditActionSubmit,
FromStatus: fromStatus,
ToStatus: SupplierApplicationStatusPending,
Reason: "申请资料已修改并重新提交",
CreatedAt: now,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
app.CompanyName = req.CompanyName
app.CreditCode = req.CreditCode
app.BusinessLicenseURL = req.BusinessLicenseURL
app.BusinessLicenseFile = req.BusinessLicenseFile
app.CompanyLogoURL = req.CompanyLogoURL
app.LegalRepresentative = req.LegalRepresentative
app.CompanySize = req.CompanySize
app.ContactName = req.ContactName
app.ContactMobile = req.ContactMobile
app.ContactWechat = req.ContactWechat
app.Status = SupplierApplicationStatusPending
app.ReviewReason = ""
app.ReviewedBy = 0
app.ReviewedAt = 0
app.UpdatedAt = now
if err := tx.Commit().Error; err != nil {
return nil, err
}
return &app, nil
}
// AdminUpdateSupplierApplication 管理员更新指定供应商申请资料。
// 与用户侧不同:即使申请处于审核通过状态,也允许管理员更新;更新后保持原状态不变。
func AdminUpdateSupplierApplication(applicationID int, req *SupplierApplication) (*SupplierApplication, error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var app SupplierApplication
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", applicationID).
First(&app).Error; err != nil {
tx.Rollback()
return nil, err
}
now := time.Now().Unix()
aliasVal := ""
if req.SupplierAlias != nil {
aliasVal = strings.TrimSpace(*req.SupplierAlias)
}
if aliasVal == "" {
aliasVal = SupplierApplicationAutoAlias(applicationID)
}
updates := map[string]any{
"company_name": req.CompanyName,
"credit_code": req.CreditCode,
"business_license_url": req.BusinessLicenseURL,
"business_license_file": req.BusinessLicenseFile,
"company_logo_url": req.CompanyLogoURL,
"supplier_type": req.SupplierType,
"legal_representative": req.LegalRepresentative,
"company_size": req.CompanySize,
"contact_name": req.ContactName,
"contact_mobile": req.ContactMobile,
"contact_wechat": req.ContactWechat,
"supplier_alias": aliasVal,
"updated_at": now,
}
if err := tx.Model(&SupplierApplication{}).
Where("id = ?", app.ID).
Updates(updates).Error; err != nil {
tx.Rollback()
return nil, err
}
app.CompanyName = req.CompanyName
app.CreditCode = req.CreditCode
app.BusinessLicenseURL = req.BusinessLicenseURL
app.BusinessLicenseFile = req.BusinessLicenseFile
app.CompanyLogoURL = req.CompanyLogoURL
app.SupplierType = req.SupplierType
app.LegalRepresentative = req.LegalRepresentative
app.CompanySize = req.CompanySize
app.ContactName = req.ContactName
app.ContactMobile = req.ContactMobile
app.ContactWechat = req.ContactWechat
savedAlias := aliasVal
app.SupplierAlias = &savedAlias
app.UpdatedAt = now
if err := tx.Commit().Error; err != nil {
return nil, err
}
return &app, nil
}
// ListApprovedSuppliersForPricing 查询定价页使用的已审核通过供应商列表。
func ListApprovedSuppliersForPricing() ([]SupplierSimplePricingItem, error) {
items := make([]SupplierSimplePricingItem, 0)
err := DB.Model(&SupplierApplication{}).
Select("id as supplier_id, company_name as supplier_name").
Where("status = ?", SupplierApplicationStatusApproved).
Order("id desc").
Scan(&items).Error
if err != nil {
return nil, err
}
return items, nil
}
// GetApprovedSupplierApplicationByApplicant 获取当前用户的审核通过供应商申请。
func GetApprovedSupplierApplicationByApplicant(applicantUserID int) (*SupplierApplication, error) {
var app SupplierApplication
err := DB.Where("applicant_user_id = ? AND status = ?", applicantUserID, SupplierApplicationStatusApproved).
Order("id desc").
First(&app).Error
if err != nil {
return nil, err
}
return &app, nil
}
// ReviewSupplierApplication 审核申请(仅允许从待审核状态流转到通过/驳回)。
// supplierAlias 为可选:通过时若为空则写入默认 P+id否则写入管理员指定别名。
func ReviewSupplierApplication(applicationID int, reviewerUserID int, toStatus int, reason string, supplierAlias string, supplierType string) (*SupplierApplication, error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var app SupplierApplication
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", applicationID).
First(&app).Error; err != nil {
tx.Rollback()
return nil, err
}
if app.Status != SupplierApplicationStatusPending {
tx.Rollback()
return nil, ErrSupplierApplicationAlreadyReviewed
}
now := time.Now().Unix()
updates := map[string]any{
"status": toStatus,
"review_reason": reason,
"reviewed_by": reviewerUserID,
"reviewed_at": now,
"updated_at": now,
}
if toStatus == SupplierApplicationStatusApproved {
trimmedAlias := strings.TrimSpace(supplierAlias)
trimmedType := strings.TrimSpace(supplierType)
if trimmedAlias != "" {
updates["supplier_alias"] = trimmedAlias
} else {
updates["supplier_alias"] = SupplierApplicationAutoAlias(applicationID)
}
updates["supplier_type"] = trimmedType
}
result := tx.Model(&SupplierApplication{}).
Where("id = ? AND status = ?", applicationID, SupplierApplicationStatusPending).
Updates(updates)
if result.Error != nil {
tx.Rollback()
return nil, result.Error
}
if result.RowsAffected == 0 {
tx.Rollback()
return nil, ErrSupplierApplicationAlreadyReviewed
}
action := SupplierApplicationAuditActionApprove
if toStatus == SupplierApplicationStatusRejected {
action = SupplierApplicationAuditActionReject
}
if err := tx.Create(&SupplierApplicationAudit{
ApplicationID: app.ID,
OperatorUserID: reviewerUserID,
Action: action,
FromStatus: SupplierApplicationStatusPending,
ToStatus: toStatus,
Reason: reason,
CreatedAt: now,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
if toStatus == SupplierApplicationStatusApproved {
// 审核通过后,回填用户表 supplier_id建立用户与供应商关联。
if err := tx.Model(&User{}).Where("id = ?", app.ApplicantUserID).
Updates(map[string]any{
"supplier_id": app.ID,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
}
app.Status = toStatus
app.ReviewReason = reason
app.ReviewedBy = reviewerUserID
app.ReviewedAt = now
app.UpdatedAt = now
if toStatus == SupplierApplicationStatusApproved {
trimmedAlias := strings.TrimSpace(supplierAlias)
trimmedType := strings.TrimSpace(supplierType)
if trimmedAlias != "" {
app.SupplierAlias = &trimmedAlias
} else {
app.SupplierAlias = ptrSupplierApplicationAlias(applicationID)
}
app.SupplierType = trimmedType
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
return &app, nil
}
// DeactivateSupplierApplication 注销供应商(仅允许审核通过状态)。
// 管理员role>=RoleAdminUser可按ID注销任意供应商普通用户仅可注销自己提交的供应商。
// 注销后会将 supplier_applications.status 置为已注销,并清空申请人用户表 supplier_id。
func DeactivateSupplierApplication(operatorUserID int, operatorRole int, supplierID int, reason string) (*SupplierApplication, error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var app SupplierApplication
query := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", supplierID)
if operatorRole < common.RoleAdminUser {
query = query.Where("applicant_user_id = ?", operatorUserID)
}
if err := query.First(&app).Error; err != nil {
tx.Rollback()
return nil, err
}
if app.Status != SupplierApplicationStatusApproved {
tx.Rollback()
return nil, ErrSupplierApplicationStatusNotApproved
}
now := time.Now().Unix()
if err := tx.Model(&SupplierApplication{}).
Where("id = ?", app.ID).
Updates(map[string]any{
"status": SupplierApplicationStatusDeactivated,
"review_reason": strings.TrimSpace(reason),
"updated_at": now,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Model(&User{}).Where("id = ?", app.ApplicantUserID).
Updates(map[string]any{
"supplier_id": 0,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Create(&SupplierApplicationAudit{
ApplicationID: app.ID,
OperatorUserID: operatorUserID,
Action: SupplierApplicationAuditActionDeactivate,
FromStatus: SupplierApplicationStatusApproved,
ToStatus: SupplierApplicationStatusDeactivated,
Reason: strings.TrimSpace(reason),
CreatedAt: now,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
app.Status = SupplierApplicationStatusDeactivated
app.ReviewReason = strings.TrimSpace(reason)
app.UpdatedAt = now
if err := tx.Commit().Error; err != nil {
return nil, err
}
return &app, nil
}
// ActivateSupplierApplication 重新启用已注销供应商(仅允许已注销状态)。
// 管理员role>=RoleAdminUser可按ID启用任意供应商普通用户仅可启用自己提交的供应商。
// 启用后会将 supplier_applications.status 置为审核通过,并回填申请人用户表 supplier_id。
func ActivateSupplierApplication(operatorUserID int, operatorRole int, supplierID int, reason string) (*SupplierApplication, error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
var app SupplierApplication
query := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", supplierID)
if operatorRole < common.RoleAdminUser {
query = query.Where("applicant_user_id = ?", operatorUserID)
}
if err := query.First(&app).Error; err != nil {
tx.Rollback()
return nil, err
}
if app.Status != SupplierApplicationStatusDeactivated {
tx.Rollback()
return nil, ErrSupplierApplicationStatusNotDeactivated
}
now := time.Now().Unix()
trimmedReason := strings.TrimSpace(reason)
if err := tx.Model(&SupplierApplication{}).
Where("id = ?", app.ID).
Updates(map[string]any{
"status": SupplierApplicationStatusApproved,
"review_reason": trimmedReason,
"updated_at": now,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Model(&User{}).Where("id = ?", app.ApplicantUserID).
Updates(map[string]any{
"supplier_id": app.ID,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Create(&SupplierApplicationAudit{
ApplicationID: app.ID,
OperatorUserID: operatorUserID,
Action: SupplierApplicationAuditActionActivate,
FromStatus: SupplierApplicationStatusDeactivated,
ToStatus: SupplierApplicationStatusApproved,
Reason: trimmedReason,
CreatedAt: now,
}).Error; err != nil {
tx.Rollback()
return nil, err
}
app.Status = SupplierApplicationStatusApproved
app.ReviewReason = trimmedReason
app.UpdatedAt = now
if err := tx.Commit().Error; err != nil {
return nil, err
}
return &app, nil
}
// CreateUserMessage 创建站内消息。
func CreateUserMessage(msg *UserMessage) error {
msg.CreatedAt = time.Now().Unix()
return DB.Create(msg).Error
}
// ListUserMessagesForUser 分页查询当前用户可见消息。
// readStatus: all/read/unreadtitleKeyword: 标题模糊查询关键字。
func ListUserMessagesForUser(userID int, role int, pageInfo *common.PageInfo, titleKeyword string, readStatus string) ([]*UserMessage, int64, error) {
var (
items []*UserMessage
total int64
)
query := DB.Model(&UserMessage{}).Where("receiver_user_id = ? OR (receiver_user_id = 0 AND receiver_min_role > 0 AND receiver_min_role <= ?)", userID, role)
if strings.TrimSpace(titleKeyword) != "" {
query = query.Where("title LIKE ?", "%"+strings.TrimSpace(titleKeyword)+"%")
}
if readStatus == "read" {
query = query.Where("(receiver_user_id = ? AND is_read = ?) OR (receiver_user_id = 0 AND EXISTS (SELECT 1 FROM user_message_reads umr WHERE umr.message_id = user_messages.id AND umr.user_id = ?))", userID, true, userID)
} else if readStatus == "unread" {
query = query.Where("(receiver_user_id = ? AND is_read = ?) OR (receiver_user_id = 0 AND NOT EXISTS (SELECT 1 FROM user_message_reads umr WHERE umr.message_id = user_messages.id AND umr.user_id = ?))", userID, false, userID)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("id desc").
Limit(pageInfo.GetPageSize()).
Offset(pageInfo.GetStartIdx()).
Find(&items).Error; err != nil {
return nil, 0, err
}
broadcastIDs := make([]int, 0)
for _, item := range items {
if item.ReceiverUserID == 0 {
broadcastIDs = append(broadcastIDs, item.ID)
}
}
if len(broadcastIDs) > 0 {
var readRows []UserMessageRead
if err := DB.Model(&UserMessageRead{}).
Select("message_id").
Where("user_id = ? AND message_id IN ?", userID, broadcastIDs).
Find(&readRows).Error; err != nil {
return nil, 0, err
}
readMap := make(map[int]bool, len(readRows))
for _, row := range readRows {
readMap[row.MessageID] = true
}
for _, item := range items {
if item.ReceiverUserID == 0 {
item.IsRead = readMap[item.ID]
}
}
}
return items, total, nil
}
// CountUnreadUserMessages 统计当前用户未读消息(支持广播消息按用户已读追踪)。
func CountUnreadUserMessages(userID int, role int) (int64, error) {
var (
directTotal int64
broadcastTotal int64
)
if err := DB.Model(&UserMessage{}).
Where("receiver_user_id = ? AND is_read = ?", userID, false).
Count(&directTotal).Error; err != nil {
return 0, err
}
if err := DB.Model(&UserMessage{}).
Where("receiver_user_id = 0 AND receiver_min_role > 0 AND receiver_min_role <= ? AND NOT EXISTS (SELECT 1 FROM user_message_reads umr WHERE umr.message_id = user_messages.id AND umr.user_id = ?)", role, userID).
Count(&broadcastTotal).Error; err != nil {
return 0, err
}
return directTotal + broadcastTotal, nil
}
// MarkUserMessageAsRead 将用户可见消息标记为已读。
// 定向消息更新 user_messages.is_read广播消息写入 user_message_reads 用户已读记录。
func MarkUserMessageAsRead(messageID int, userID int, role int) (bool, error) {
var msg UserMessage
if err := DB.Where("id = ? AND (receiver_user_id = ? OR (receiver_user_id = 0 AND receiver_min_role > 0 AND receiver_min_role <= ?))", messageID, userID, role).
First(&msg).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
now := time.Now().Unix()
if msg.ReceiverUserID == userID {
res := DB.Model(&UserMessage{}).
Where("id = ? AND receiver_user_id = ? AND is_read = ?", messageID, userID, false).
Updates(map[string]any{
"is_read": true,
"read_at": now,
})
if res.Error != nil {
return false, res.Error
}
return res.RowsAffected > 0, nil
}
read := UserMessageRead{
UserID: userID,
MessageID: messageID,
ReadAt: now,
CreatedAt: now,
}
res := DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "message_id"}},
DoUpdates: clause.Assignments(map[string]any{"read_at": now}),
}).Create(&read)
if res.Error != nil {
return false, res.Error
}
return true, nil
}
// MarkAllUserMessagesAsRead 将当前用户可见消息全部标记为已读。
// 定向消息写回 user_messages广播消息写入 user_message_reads。
func MarkAllUserMessagesAsRead(userID int, role int) (int64, error) {
tx := DB.Begin()
if tx.Error != nil {
return 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
now := time.Now().Unix()
res := tx.Model(&UserMessage{}).
Where("receiver_user_id = ? AND is_read = ?", userID, false).
Updates(map[string]any{
"is_read": true,
"read_at": now,
})
if res.Error != nil {
tx.Rollback()
return 0, res.Error
}
updatedCount := res.RowsAffected
var broadcastIDs []int
if err := tx.Model(&UserMessage{}).
Select("id").
Where("receiver_user_id = 0 AND receiver_min_role > 0 AND receiver_min_role <= ?", role).
Find(&broadcastIDs).Error; err != nil {
tx.Rollback()
return 0, err
}
if len(broadcastIDs) > 0 {
reads := make([]UserMessageRead, 0, len(broadcastIDs))
for _, messageID := range broadcastIDs {
reads = append(reads, UserMessageRead{
UserID: userID,
MessageID: messageID,
ReadAt: now,
CreatedAt: now,
})
}
insertRes := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "message_id"}},
DoUpdates: clause.Assignments(map[string]any{"read_at": now}),
}).Create(&reads)
if insertRes.Error != nil {
tx.Rollback()
return 0, insertRes.Error
}
updatedCount += insertRes.RowsAffected
}
if err := tx.Commit().Error; err != nil {
return 0, err
}
return updatedCount, nil
}
// IsSupplierApplicationNotFound 判断是否未找到供应商申请记录。
func IsSupplierApplicationNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}
// BackfillSupplierApplicationAlias 将 supplier_alias 统一为 P+id迁移/修复用,可安全重复执行)。
func BackfillSupplierApplicationAlias() error {
switch {
case common.UsingMySQL:
return DB.Exec("UPDATE supplier_applications SET supplier_alias = CONCAT('P', id) WHERE id > 0").Error
case common.UsingPostgreSQL:
return DB.Exec(`UPDATE supplier_applications SET supplier_alias = 'P' || id::text WHERE id > 0`).Error
default:
return DB.Exec("UPDATE supplier_applications SET supplier_alias = 'P' || id WHERE id > 0").Error
}
}
// IsSupplierCreditCodeDuplicateError 判断是否为统一社会信用代码重复错误。
func IsSupplierCreditCodeDuplicateError(err error) bool {
if err == nil {
return false
}
lowerMsg := strings.ToLower(err.Error())
if !strings.Contains(lowerMsg, "credit_code") {
return false
}
// 兼容 MySQL / PostgreSQL / SQLite 常见唯一约束错误文案
return strings.Contains(lowerMsg, "duplicate") ||
strings.Contains(lowerMsg, "duplicated") ||
strings.Contains(lowerMsg, "unique constraint") ||
strings.Contains(lowerMsg, "unique failed") ||
strings.Contains(lowerMsg, "idx_supplier_applications_credit_code")
}
// IsSupplierAliasDuplicateError 判断是否为供应商别名重复错误。
func IsSupplierAliasDuplicateError(err error) bool {
if err == nil {
return false
}
lowerMsg := strings.ToLower(err.Error())
if !strings.Contains(lowerMsg, "supplier_alias") {
return false
}
// 兼容 MySQL / PostgreSQL / SQLite 常见唯一约束错误文案
return strings.Contains(lowerMsg, "duplicate") ||
strings.Contains(lowerMsg, "duplicated") ||
strings.Contains(lowerMsg, "unique constraint") ||
strings.Contains(lowerMsg, "unique failed") ||
strings.Contains(lowerMsg, "idx_supplier_applications_supplier_alias")
}