964 lines
35 KiB
Go
964 lines
35 KiB
Go
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/unread;titleKeyword: 标题模糊查询关键字。
|
||
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")
|
||
}
|