更新若干功能

This commit is contained in:
hero920103 committed 2026-10-21 17:45:50 +08:00
1 parent 892ba5886d
commit 79d5ae6b66
45 files changed
+6755 -38

No files matched your search

+237
View File
@@ -0,0 +1,237 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"server/pkg/identity"
"server/services/dailyreport"
)
// OA 工作日报(小程序端)。与后台 /backend/oa/dailyreport/* 共用
// services/dailyreport 的权限与校验逻辑,仅在响应包装与上传入口上有差异。
// dailyReportCtx 解析移动端令牌并组装日报身份。
func (c *AppOaController) dailyReportCtx() (*dailyreport.UserContext, bool) {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return nil, false
}
u, err := dailyreport.BuildContext(
uint64(claims.TenantId), uint64(claims.UserID), claims.Username, claims.UserType == "platform")
if err != nil {
code := dailyreport.StatusOf(err, 500)
msg := err.Error()
if code == 500 {
msg = "操作失败,请稍后重试"
}
c.oaErr(code, code, msg)
return nil, false
}
return u, true
}
func (c *AppOaController) dailyReportFail(err error) {
code := dailyreport.StatusOf(err, 500)
msg := err.Error()
if code == 500 {
msg = "操作失败,请稍后重试"
}
c.oaErr(code, code, msg)
}
func (c *AppOaController) dailyReportID() uint64 {
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if id == 0 {
c.oaErr(400, 400, "无效ID")
}
return id
}
// DailyReportProfile GET /api/oa/dailyreport/profile
func (c *AppOaController) DailyReportProfile() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
teamOrgs := u.TeamOrgIDs
if teamOrgs == nil {
teamOrgs = []uint64{}
}
c.oaOk(map[string]interface{}{
"user_id": u.UID,
"user_name": u.Name,
"org_id": u.OrgID,
"org_name": u.OrgName,
"is_admin": u.IsAdmin,
"can_view_team": u.CanViewTeam,
"team_org_ids": teamOrgs,
"window_start": dailyreport.WindowStart(),
"window_end": dailyreport.Today(),
"backfill_days": dailyreport.BackfillDays,
})
}
// DailyReportMineList GET /api/oa/dailyreport/mine/list
func (c *AppOaController) DailyReportMineList() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
res, err := dailyreport.ListMine(u, dailyreport.MineQuery{
StartDate: strings.TrimSpace(c.GetString("start_date")),
EndDate: strings.TrimSpace(c.GetString("end_date")),
Page: page,
PageSize: pageSize,
})
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(res)
}
// DailyReportSave POST /api/oa/dailyreport/save
func (c *AppOaController) DailyReportSave() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
var in dailyreport.SaveInput
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &in) != nil {
c.oaErr(400, 400, "参数错误")
return
}
item, err := dailyreport.Save(u, in)
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(item)
}
// DailyReportDetail GET /api/oa/dailyreport/detail/:id
func (c *AppOaController) DailyReportDetail() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
id := c.dailyReportID()
if id == 0 {
return
}
detail, err := dailyreport.GetDetail(u, id)
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(detail)
}
// DailyReportDelete DELETE /api/oa/dailyreport/delete/:id
func (c *AppOaController) DailyReportDelete() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
id := c.dailyReportID()
if id == 0 {
return
}
if err := dailyreport.Delete(u, id); err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(map[string]interface{}{"id": id})
}
// DailyReportTeamStatus GET /api/oa/dailyreport/team/status
func (c *AppOaController) DailyReportTeamStatus() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
orgID, _ := c.GetInt64("org_id", 0)
res, err := dailyreport.TeamStatus(u, dailyreport.TeamQuery{
Date: strings.TrimSpace(c.GetString("date")),
OrgID: uint64(orgID),
Keyword: strings.TrimSpace(c.GetString("keyword")),
Page: page,
PageSize: pageSize,
})
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(res)
}
// DailyReportTeamOrgs GET /api/oa/dailyreport/team/orgs
func (c *AppOaController) DailyReportTeamOrgs() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
orgs, err := dailyreport.TeamOrgs(u)
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(map[string]interface{}{"list": orgs})
}
// DailyReportComment POST /api/oa/dailyreport/comment
func (c *AppOaController) DailyReportComment() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
var payload oaDailyReportCommentPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &payload) != nil {
c.oaErr(400, 400, "参数错误")
return
}
if payload.ReportID == 0 {
c.oaErr(400, 400, "缺少日报ID")
return
}
comment, err := dailyreport.AddComment(u, payload.ReportID, payload.Content, payload.ReplyToID)
if err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(comment)
}
// DailyReportCommentDelete DELETE /api/oa/dailyreport/comment/:id
func (c *AppOaController) DailyReportCommentDelete() {
u, ok := c.dailyReportCtx()
if !ok {
return
}
id := c.dailyReportID()
if id == 0 {
return
}
if err := dailyreport.DeleteComment(u, id); err != nil {
c.dailyReportFail(err)
return
}
c.oaOk(map[string]interface{}{"id": id})
}
// DailyReportUpload POST /api/oa/dailyreport/upload(multipart,字段名 file)
// 复用 OA 附件上传:图片 / PDF,单个文件 ≤10MB,落入租户文件管理。
func (c *AppOaController) DailyReportUpload() {
c.uploadTenantImage()
}
+80
View File
@@ -348,6 +348,10 @@ func (c *BackendCrmBusinessController) ChangeStage() {
pipelineErr(&c.Controller, 400, 400, "商机已转化并锁定,不能流转阶段")
return
}
if biz.Status == 3 {
pipelineErr(&c.Controller, 400, 400, "商机已废弃,请先恢复为「跟进中」再流转阶段")
return
}
// 阶段未变化:直接幂等返回,不写日志
if strings.TrimSpace(biz.Stage) == stage {
pipelineOk(&c.Controller, map[string]interface{}{"id": biz.ID, "stage": biz.Stage})
@@ -364,6 +368,78 @@ func (c *BackendCrmBusinessController) ChangeStage() {
pipelineOk(&c.Controller, map[string]interface{}{"id": biz.ID, "stage": stage})
}
// ChangeStatus POST /backend/crm/business/:id/status
// 商机状态流转:1跟进中 / 3已废弃。
// 「已转化项目(2)」不走本接口,须用「转项目」完成(会锁定商机)。
func (c *BackendCrmBusinessController) ChangeStatus() {
claims, err := pipelineClaims(&c.Controller)
if err != nil {
code := identity.Status(err)
pipelineErr(&c.Controller, code, code, err.Error())
return
}
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if id == 0 {
pipelineErr(&c.Controller, 400, 400, "无效的ID")
return
}
raw, _ := io.ReadAll(c.Ctx.Request.Body)
var p struct {
Status int8 `json:"status"`
}
if err := json.Unmarshal(raw, &p); err != nil {
pipelineErr(&c.Controller, 400, 400, "参数错误")
return
}
if p.Status != 1 && p.Status != 3 {
pipelineErr(&c.Controller, 400, 400, "仅支持流转为「跟进中 / 已废弃」,转为已转化项目请使用「转项目」")
return
}
tenantID := pipelineTenantID(claims)
var biz models.TenantCrmBusiness
if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
Filter("id", id).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&biz); err != nil {
pipelineErr(&c.Controller, 404, 404, "商机未找到")
return
}
// 数据范围校验:非管理员仅能操作本人(负责人或创建人)的数据
if !canAccessCrmRecord(claims, biz.OwnerUserID, biz.CreateUserID) {
pipelineErr(&c.Controller, 403, 403, "无权操作该商机数据")
return
}
if biz.Locked == 1 || biz.Status == 2 {
pipelineErr(&c.Controller, 400, 400, "商机已转化并锁定,不能流转状态")
return
}
// 状态未变化:直接幂等返回,不写日志
if biz.Status == p.Status {
pipelineOk(&c.Controller, map[string]interface{}{"id": biz.ID, "status": biz.Status})
return
}
if _, err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
Filter("id", id).Filter("tenant_id", tenantID).
Update(map[string]interface{}{"status": p.Status, "update_time": time.Now()}); err != nil {
pipelineErr(&c.Controller, 500, 500, "状态更新失败: "+err.Error())
return
}
crmWriteLog(tenantID, 2, biz.ID, "status",
fmt.Sprintf("商机状态:%s → %s", businessStatusName(biz.Status), businessStatusName(p.Status)), claims)
pipelineOk(&c.Controller, map[string]interface{}{"id": biz.ID, "status": p.Status})
}
// businessStatusName 商机状态文案(用于操作日志):1跟进中 / 2已转化项目 / 3已废弃
func businessStatusName(s int8) string {
switch s {
case 2:
return "已转化项目"
case 3:
return "已废弃"
default:
return "跟进中"
}
}
// businessStageName 商机阶段文案(用于操作日志):1初步接洽 … 5赢单 / 6输单
func businessStageName(s string) string {
switch strings.TrimSpace(s) {
@@ -480,6 +556,10 @@ func (c *BackendCrmBusinessController) Convert() {
pipelineErr(&c.Controller, 400, 400, "商机已转化,不能重复转化")
return
}
if biz.Status == 3 {
pipelineErr(&c.Controller, 400, 400, "商机已废弃,请先恢复为「跟进中」再转项目")
return
}
projectName := strings.TrimSpace(p.ProjectName)
if projectName == "" {
+4
View File
@@ -475,6 +475,10 @@ func (c *BackendCrmClueController) Convert() {
pipelineErr(&c.Controller, 400, 400, "线索已转化,不能重复转化")
return
}
if clue.Status == 3 {
pipelineErr(&c.Controller, 400, 400, "线索已废弃,请先恢复为「跟进中」再转化商机")
return
}
// 1. 处理客户:新建正式客户 或 关联已有客户
customerName := strings.TrimSpace(p.Customer.CustomerName)
+8 -5
View File
@@ -57,12 +57,15 @@ func isTenantAdmin(userID int, tenantID int) bool {
if tu.GroupID == 0 {
return true
}
// 「租户管理员」「租户用户」是 cid=2 且 tenant_id=0 的全局预置角色,
// 只有租户自建角色才带自己的 tenant_id,因此两种归属都要接受,
// 否则全局角色会因 tenant_id 不匹配而永远查不到角色。
cond := orm.NewCondition().
And("id", tu.GroupID).
And("cid", 2).
AndCond(orm.NewCondition().Or("tenant_id", 0).Or("tenant_id", tenantID))
var role models.AdminRole
if err := models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", tu.GroupID).
Filter("cid", 2).
Filter("tenant_id", tenantID).
One(&role); err != nil {
if err := models.Orm.QueryTable(new(models.AdminRole)).SetCond(cond).One(&role); err != nil {
return false
}
if role.Name == "租户管理员" {
+261
View File
@@ -0,0 +1,261 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services/dailyreport"
beego "github.com/beego/beego/v2/server/web"
)
// BackendOaDailyReportController OA工作日报(后台 PC 端)。
//
// 可见范围由 services/dailyreport 统一判定:
// - 本人:自己的全部日报(含草稿),补写窗口内可编辑/删除;
// - 部门主管:本人作为负责人的部门及其下级部门成员「已提交」的日报;
// - 租户管理员:全租户日报,可删除任意日报与评论。
type BackendOaDailyReportController struct {
beego.Controller
}
func (c *BackendOaDailyReportController) odrClaims() (*jwtutil.Claims, error) {
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndBackend)
}
func (c *BackendOaDailyReportController) odrErr(status, bizCode int, msg string) {
c.Ctx.Output.SetStatus(status)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendOaDailyReportController) odrOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
func (c *BackendOaDailyReportController) odrFail(err error) {
code := dailyreport.StatusOf(err, 500)
msg := err.Error()
if code == 500 {
msg = "操作失败,请稍后重试"
}
c.odrErr(code, code, msg)
}
// odrCtx 解析令牌并组装日报身份;失败时已输出响应。
func (c *BackendOaDailyReportController) odrCtx() (*dailyreport.UserContext, bool) {
claims, err := c.odrClaims()
if err != nil {
code := identity.Status(err)
c.odrErr(code, code, err.Error())
return nil, false
}
u, err := dailyreport.BuildContext(
uint64(claims.TenantId), uint64(claims.UserID), claims.Username, claims.UserType == "platform")
if err != nil {
c.odrFail(err)
return nil, false
}
return u, true
}
func (c *BackendOaDailyReportController) odrIDParam(name string) uint64 {
id, _ := strconv.ParseUint(c.Ctx.Input.Param(name), 10, 64)
if id == 0 {
c.odrErr(400, 400, "无效ID")
}
return id
}
// Profile GET /backend/oa/dailyreport/profile
// 返回当前用户身份、补写窗口与团队可见性,供页面决定是否显示「团队日报」Tab。
func (c *BackendOaDailyReportController) Profile() {
u, ok := c.odrCtx()
if !ok {
return
}
teamOrgs := u.TeamOrgIDs
if teamOrgs == nil {
teamOrgs = []uint64{}
}
c.odrOk(map[string]interface{}{
"user_id": u.UID,
"user_name": u.Name,
"org_id": u.OrgID,
"org_name": u.OrgName,
"is_admin": u.IsAdmin,
"can_view_team": u.CanViewTeam,
"team_org_ids": teamOrgs,
"window_start": dailyreport.WindowStart(),
"window_end": dailyreport.Today(),
"backfill_days": dailyreport.BackfillDays,
})
}
// MineList GET /backend/oa/dailyreport/mine/list
// 我的日报分页列表,支持 start_date/end_date 筛选。
func (c *BackendOaDailyReportController) MineList() {
u, ok := c.odrCtx()
if !ok {
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
res, err := dailyreport.ListMine(u, dailyreport.MineQuery{
StartDate: strings.TrimSpace(c.GetString("start_date")),
EndDate: strings.TrimSpace(c.GetString("end_date")),
Page: page,
PageSize: pageSize,
})
if err != nil {
c.odrFail(err)
return
}
c.odrOk(res)
}
// Save POST /backend/oa/dailyreport/save
// 新建或更新日报;submit=true 提交,false 存草稿。
func (c *BackendOaDailyReportController) Save() {
u, ok := c.odrCtx()
if !ok {
return
}
var in dailyreport.SaveInput
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &in) != nil {
c.odrErr(400, 400, "参数错误")
return
}
item, err := dailyreport.Save(u, in)
if err != nil {
c.odrFail(err)
return
}
c.odrOk(item)
}
// Detail GET /backend/oa/dailyreport/detail/:id
func (c *BackendOaDailyReportController) Detail() {
u, ok := c.odrCtx()
if !ok {
return
}
id := c.odrIDParam(":id")
if id == 0 {
return
}
detail, err := dailyreport.GetDetail(u, id)
if err != nil {
c.odrFail(err)
return
}
c.odrOk(detail)
}
// Delete DELETE /backend/oa/dailyreport/delete/:id
func (c *BackendOaDailyReportController) Delete() {
u, ok := c.odrCtx()
if !ok {
return
}
id := c.odrIDParam(":id")
if id == 0 {
return
}
if err := dailyreport.Delete(u, id); err != nil {
c.odrFail(err)
return
}
c.odrOk(map[string]interface{}{"id": id})
}
// TeamStatus GET /backend/oa/dailyreport/team/status
// 团队日报汇总:date/org_id/keyword/page/pageSize。
func (c *BackendOaDailyReportController) TeamStatus() {
u, ok := c.odrCtx()
if !ok {
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
orgID, _ := c.GetInt64("org_id", 0)
res, err := dailyreport.TeamStatus(u, dailyreport.TeamQuery{
Date: strings.TrimSpace(c.GetString("date")),
OrgID: uint64(orgID),
Keyword: strings.TrimSpace(c.GetString("keyword")),
Page: page,
PageSize: pageSize,
})
if err != nil {
c.odrFail(err)
return
}
c.odrOk(res)
}
// TeamOrgs GET /backend/oa/dailyreport/team/orgs
func (c *BackendOaDailyReportController) TeamOrgs() {
u, ok := c.odrCtx()
if !ok {
return
}
orgs, err := dailyreport.TeamOrgs(u)
if err != nil {
c.odrFail(err)
return
}
c.odrOk(map[string]interface{}{"list": orgs})
}
type oaDailyReportCommentPayload struct {
ReportID uint64 `json:"report_id"`
Content string `json:"content"`
ReplyToID uint64 `json:"reply_to_id"`
}
// Comment POST /backend/oa/dailyreport/comment
func (c *BackendOaDailyReportController) Comment() {
u, ok := c.odrCtx()
if !ok {
return
}
var payload oaDailyReportCommentPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &payload) != nil {
c.odrErr(400, 400, "参数错误")
return
}
if payload.ReportID == 0 {
c.odrErr(400, 400, "缺少日报ID")
return
}
comment, err := dailyreport.AddComment(u, payload.ReportID, payload.Content, payload.ReplyToID)
if err != nil {
c.odrFail(err)
return
}
c.odrOk(comment)
}
// DeleteComment DELETE /backend/oa/dailyreport/comment/:id
func (c *BackendOaDailyReportController) DeleteComment() {
u, ok := c.odrCtx()
if !ok {
return
}
id := c.odrIDParam(":id")
if id == 0 {
return
}
if err := dailyreport.DeleteComment(u, id); err != nil {
c.odrFail(err)
return
}
c.odrOk(map[string]interface{}{"id": id})
}
+40
View File
@@ -0,0 +1,40 @@
package controllers
import (
"server/services/wechatmini"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 官网前台只读接口:小程序公开信息
//
// 供 website SPA / H5 / 其他端调取平台端「小程序配置」里维护的
// 小程序名称、AppID 与二维码图片地址,无需登录。
// GET /site/miniapp 已启用小程序列表(含 qrcode_url)
//
// 说明:qrcode_url 形如 /uploads/xxx(本地存储)或 CDN 绝对地址,
// 相对地址由前端拼接接口域名。
// =============================================================
// IndexMiniAppController 小程序公开信息
type IndexMiniAppController struct {
beego.Controller
}
// List GET /site/miniapp 已启用小程序列表(含二维码图片地址)
func (c *IndexMiniAppController) List() {
list, err := wechatmini.ListEnabledApps()
if err != nil {
c.Ctx.Output.SetStatus(500)
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "获取小程序列表失败"}
_ = c.ServeJSON()
return
}
c.Data["json"] = map[string]interface{}{
"code": 200,
"msg": "success",
"data": map[string]interface{}{"list": list},
}
_ = c.ServeJSON()
}
+2
View File
@@ -155,6 +155,7 @@ func (c *PlatformWechatMiniController) SaveApp() {
Name string `json:"name"`
AppID string `json:"app_id"`
AppSecret string `json:"app_secret"`
QrcodeURL string `json:"qrcode_url"`
Enabled int `json:"enabled"`
AutoBind int `json:"auto_bind"`
Remark string `json:"remark"`
@@ -177,6 +178,7 @@ func (c *PlatformWechatMiniController) SaveApp() {
Name: p.Name,
AppID: p.AppID,
AppSecret: p.AppSecret,
QrcodeURL: p.QrcodeURL,
Enabled: p.Enabled == 1,
AutoBind: p.AutoBind == 1,
Remark: p.Remark,